8
votes

I'm transitioning from manual memory management to ARC and have an issue. Most of the time, I'm performing data load asynchronously by calling performSelectorInBackground in my model classes. The thing is I need to stop any model code execution when model receives nil (release). In non-arc, everything was straightforward - as soon as a user closes the window, its controller starts to deallocate itself and deallocates its model [_myModel release], and so model stops its code execution (data loading) and gets called its dealloc method.

This seems to be different in ARC. Model still executes the code even after receiving nil message from controller. Its dealloc method gets called after its code execution (data load) only. This is an issue because the code execution should stop ASAP when a user closes the window (controller). It's some sort of a lack of control over the code - controller tells to model - "go away, I don't need your work anymore" but the model still "is working to finish its job" :).

Imagine a model performs some very heavy data processing with duration of 10 seconds. A model starts to do its processing when a user opens the window (controller). But image a user changes his mind and closes the window, just after the opening it. The model still perform wasteful processing. Any ideas how to solve or workaround that? I don't like an idea to have a special BOOL "shouldDealloc" property in my model and set to YES in controller dealloc method, and use in my model class conditions. Is there more elegant solution?

I have made some demo project to show the problem. For testing just create single view application and paste the code. Create to buttons- "Start calculate" and "Stop calculate" in ViewController.xib file, and connect their IBActions with startCalculationPressed and stopCalculationPressed:

ViewController.h

#import "MyModel.h"

@interface ViewController : UIViewController <MyModelDelegate>

- (IBAction)startCalculationPressed:(id)sender;
- (IBAction)stopCalculationPressed:(id)sender;

@end

ViewController.m

@interface ViewController (){

  __strong MyModel *_myModel;
}
@end

@implementation ViewController

- (void)viewDidLoad
{
  [super viewDidLoad];
  // Do any additional setup after loading the view, typically from a nib.
}

- (void)didReceiveMemoryWarning
{
  [super didReceiveMemoryWarning];
  // Dispose of any resources that can be recreated.
}

- (void)didCalculated
{
  NSLog(@"Did calculated...");
}

- (IBAction)startCalculationPressed:(id)sender
{
  NSLog(@"Starting to calculate...");

  _myModel = nil;
  _myModel = [[MyModel alloc] init];
  _myModel.delegate = self;

  [_myModel calculate];
}

- (IBAction)stopCalculationPressed:(id)sender
{
  NSLog(@"Stopping calculation...");
  _myModel.delegate = nil;
  _myModel = nil;
}
@end

Add new MyModel class to the project:

MyModel.h

@protocol MyModelDelegate <NSObject>

  - (void)didCalculated;

@end

@interface MyModel : NSObject

  @property (nonatomic, weak) id<MyModelDelegate> delegate;

  - (void)calculate;

@end

MyModel.m

@implementation MyModel

- (void)dealloc
{
  NSLog(@"MyModel dealloc...");
}

- (void)calculate
{
  [self performSelectorInBackground:@selector(performCalculateAsync) withObject:nil];
}

- (void)performCalculateAsync
{
  // Performing some longer running task
  int i;
  int limit = 1000000;
  NSMutableArray *myList = [[NSMutableArray alloc] initWithCapacity:limit];

  for (i = 0; i < limit; i++) {

    [myList addObject:[NSString stringWithFormat:@"Object%d", i]];
  }

  [self performSelectorOnMainThread:@selector(calculateCallback) withObject:nil waitUntilDone:NO];

}

- (void)calculateCallback
{
  [self.delegate didCalculated];
}

@end

UPDATE Martin is right, performSelectorOnMainThread always retains self, so there's no way how to stop code execution on other thread (both in ARC and non-ARC) so dealloc is not called immediately when releasing model. So, that should be done explicitly using appropriate property (for example delegate) with conditional checking.

1
why not just keep an bool that is checked in your performCalculateAsync for loop and will just end the function when it is set by stopCalculationPressed (will need some sort of mutex prob) maybe your actual program doesnt have a loop or somewhere you can place this check appropriately but if it does... - Fonix
@Fonix You see it's only a simple demo code. There are more stuff in real model classes (processing, callbacks and etc). Having such surrogate property would force me to add conditional checks all over the code making it ugly... - Centurion
You should not use memory management to control your program's behavior. Memory management is for managing memory and not anything else. If you need a thread or some other operation to terminate, you must arrange for an explicit signal to that effect that's independent of memory management. Your prior design was broken. - Ken Thomases
@Centurion You might want to take a look at NSOperation. It has built-in support for canceling asynchronous operations. - jlehr
@jlehr: A running NSOperation also has to check if it has been canceled (the documentation explicitly states "You should always support cancellation semantics in any custom code you write. In particular, your main task code should periodically check the value of the isCancelled method."), so there is no fundamental difference between performSelectorInBackground/dispatch_async/NSOperation with regard to cancellation. - Martin R

1 Answers

6
votes

An object is deallocated if its release count goes down to zero, or in ARC language, if the last strong reference to that object is gone.

[self performSelectorInBackground:@selector(performCalculateAsync) withObject:nil];

adds a strong reference to self, which explains why the object is not deallocated before the background thread has finished.

There is no way (that I know of) to make a background thread stop "automatically". The same holds true for blocks started with dispatch_async() or for NSOperation. Once started, the thread/block/operation must monitor some property at points where it is save to stop.

In your example you could monitor self.delegate. If that becomes nil, nobody is interested in the result anymore, so the background thread can return. In that case, it would make sense to declare the delegate property as atomic.

Note that self.delegate is also automatically set to nil if the view controller is deallocated (because it is a weak property) even if stopCalculationPressedhas not been called.