17

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?

  • What kind of exception are you getting? – Deepak Danduprolu Jun 15 '11 at 05:40
  • 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 Jun 17 '11 at 04:10
  • 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 Jun 26 '11 at 06:41
  • 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 Aug 11 '11 at 01:42
  • 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 Aug 08 '12 at 09:22
  • @user102008 "most of the time" is insufficient - it will result in a background dealloc sometimes, and more often than you think. I created a unit test to cover this sort of problem, and the results were quite surprising. – Airsource Ltd Jul 10 '14 at 16:56

3 Answers3

12

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
    }
});
Kazuki Sakamoto
  • 13,929
  • 2
  • 34
  • 96
2

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~

Michael Petrotta
  • 59,888
  • 27
  • 145
  • 179
winlin
  • 41
  • 2
-2

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
}
Jeff
  • 2,659
  • 1
  • 22
  • 41
  • 3
    This is not thread-safe. You can't safely assume that the block will be deallocated within 0.1 seconds (and thus that the last release call is on the main thread). Sure, the probability is vanishingly small; but it's still non-zero, and this code can crash. – Adam Ernst Aug 18 '11 at 18:42
  • Ah, the release will be on the background thread if the block has not dealloced. How would you force the release to be on the main thread? – Jeff Oct 16 '11 at 08:13