Articles

performSelectorOnMainThread vs. dispatch_async

By Jonathan Schwarz

We recently ran into a tricky bug in one of our apps. In rare cases, while the user was performing a common action, the app would fail to respond correctly, but no errors were occurring. While debugging the problem, I added some extra logging to see if I could narrow down what was happening. What I saw was even more confusing - everything was happening exactly as it was supposed to.

The gist of the issue was this:

  1. The app launches a background thread to perform an action
  2. The background action completes, and sends information to a delegate on the main thread.
  3. The action performs cleanup on the main thread.

Step 3 includes clearing the action's delegate. In the cases where the bug was occurring, the delegate was being cleared before the delegate could be notified.

Let's look at some sample code that displays the same issue. We'll create 1,000 NSOperations that each call functions on the main thread in two different ways.

Here's the operation's main method:

- (void)main
{
    [self performSelectorOnMainThread:@selector(doA) withObject:nil waitUntilDone:NO];

    dispatch_async(dispatch_get_main_queue(), ^{
        [self doB];
    });
}

It looks correct when you look at it - we're going to call doA, and then doB, both on the main thread, without waiting for either of those calls to complete. The contents of doA and doB are pretty simple:

    - (void)doA
    {
        self.aHasCompleted = YES;
    }

and

    - (void)doB
    {
        if (!self.aHasCompleted)
        {
            NSLog(@"B running without A having run yet!, %@", self);
        }
    }

With the NSOperationQueue's maxConcurrentCount set to 4, I consistently see about 40 out of 1,000 of the operations calling doB before doA, or 4% failure. That's enough to be significant while being really hard to track down.

The fix was straightforward. Changing the performSelectorOnMainThread call to use dispatch_async like the other call resolved the issue.

Why does this happen? It's likely because performSelectorOnMainThread:withObject:waitUntilDone: queues the message with common run loop modes. According to Apple's "Concurrency Programming Guide", the main queue will interleave queued tasks with other events from the app's run loop. Thus, if there are other events to be processed in the event queue, the queued blocks in the dispatch queue may be run first, even though they were submitted later.

Takeaways:

  1. Don't do this. Be consistent with your async calls. According to the documentation, multiple dispatch_async calls on same queue will have order maintained, and multiple performSelectorOnMainThread: calls from same thread will maintain order as well. As we've seen, there's no guarantee that these two different methods of calling a function on the main thread will maintain order.
  2. No, really, don't do this. Refactor your code. The best fix for this is not to be call methods asynchronously on the main thread and depend on them running in a specific order. Combining the calls so that there's only one call that needs to run on main would prevent this from happening, no matter what method was used to bounce the call to main.

Download the sample code.

Jonathan Schwarz

Jonathan Schwarz

Jonathan is a Lead Developer at Black Pixel.