17
votes

One of the patterns presented at the WWDC 2010 "Blocks and Grand Central Dispatch" talk was to use nested dispatch_async calls to perform time consuming tasks on a background thread and then update the UI on the main thread once the task is complete

dispatch_async(backgroundQueue, ^{
    // do something time consuming in background
    NSArray *results = ComputeBigKnarlyThingThatWouldBlockForAWhile();

    // use results on the main thread
    dispatch_async(dispatch_get_main_queue(), ^{
        [myViewController UpdateUiWithResults:results];
    });
});

Since "myViewController" is being used inside the blocks, it automatically gets a 'retain' and will later get a 'release' when the blocks are cleaned up.

If the block's 'release' call is the final release call (for example, the user navigates away from the view while the background task is running) the myViewController dealloc method is called -- but it's called on the background thread!!

UIKit objects do not like to be de-allocated outside of the main thread. In my case, UIWebView throws an exception.

How can this WWDC presented pattern - specifically mentioned as the best new way to avoid UI lockup - be so flawed? Am I missing something?

3
What kind of exception are you getting? - Deepak Danduprolu
I don't have it in front of me - but to paraphrase, the exception says something along the lines of not being able to obtain a lock on the main thread or the webview thread ... - Ralph Marczynski
I'm having the same problem. There error is: "bool _WebTryThreadLock(bool), 0x2eb710: Tried to obtain the web lock from a thread other than the main thread or the web thread. This may be a result of calling to UIKit from a secondary thread. Crashing now..." I've tried doing a retain and then (in the dispatch_async) doing a release via performSelectorOnMainThread, but the dispatch_async done its own retain and release, so the last release STILL happens on the background thread. - Jeff
The best solution I've been able to come up with is to check the thread in the ViewController's dealloc method. If the thread is a background thread, dispatch [self dealloc] on the main thread. - Ralph Marczynski
Shouldn't the inner block be the last one to release the view controller most of the time? And that happens on the main thread. So what is the problem? The outer block schedules the inner block on the main thread. Then the outer block finishes and is deallocated, which releases the view controller. The inner block should still be retaining the view controller, unless (very unlikely) it runs super fast and finishes before the outer block is done. So when the inner block is done, it is deallocated on the main thread and releases the view controller on the main thread. Right? - user102008

3 Answers

12
votes

You can use the __block storage type qualifier for such a case. __block variables are not automatically retained by the block. So you need to retain the object by yourself:

__block UIViewController *viewController = [myViewController retain];
dispatch_async(backgroundQueue, ^{
    // Do long-running work here.
    dispatch_async(dispatch_get_main_queue(), ^{
        [viewController updateUIWithResults:results];
        [viewController release]; // Ensure it's released on main thread
    }
});

EDIT

With ARC, __block variable object is automatically retained by the block, but we can set nil value to the __block variable for releasing the retained object whenever we want.

__block UIViewController *viewController = myViewController;
dispatch_async(backgroundQueue, ^{
    // Do long-running work here.
    dispatch_async(dispatch_get_main_queue(), ^{
        [viewController updateUIWithResults:results];
        viewController = nil; // Ensure it's released on main thread
    }
});
2
votes

In a thread, I just use [viewController retain]; then at the end of the thread use [viewController release]. It works and I don't use GCD~

-2
votes

This worked for me (added a timer):

[self retain]; // this guarantees that the last release will be on the main threaad
dispatch_async(backgroundQueue, ^{
    // do something time consuming in background
    NSArray *results = ComputeBigKnarlyThingThatWouldBlockForAWhile();

    // use results on the main thread
    dispatch_async(dispatch_get_main_queue(), ^{
        [myViewController UpdateUiWithResults:results];
        [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(releaseMe:) userInfo:nil repeats:NO];
    });
});
- (void)releaseMe:(NSTimer *)theTimer {
    [self release]; // will be on the main thread
}