Duck
Duck

Reputation: 35933

What is the difference between GCD main queue and the main thread?

I read comment on SO that dispatching a queue to the main thread is not the same as performing code on the main thread. If I understood correctly the user was saying that this

dispatch_async(dispatch_get_main_queue(),
                 ^{
                      // some code
                 });

was not the same as this

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

- (void) doStuff {
  // some code
}

is there some true about this comment?

Excluding the fact that the first code is asynchronous, for me, both codes are performed equally on the main thread. Is there any technical difference between them?

I am asking that because I had some code to update the UI using dispatch_async on the main thread and it was not working but when I changed that to the second form using performSelectorOnMainThread, it worked.

Upvotes: 11

Views: 3504

Answers (2)

Ken Thomases
Ken Thomases

Reputation: 90521

Yes, there's a difference. The main dispatch queue is a serial queue. That means that, while it's running a task that's been submitted to it, it can't run any other tasks. That's true even if it runs an inner event loop.

-performSelectorOnMainThread:... operates through a run loop source. Run loop sources can fire in an inner run loop even if that inner run loop is a result of a previous firing of that same source.

One case where this bit me is with running a modal file open dialog. (Non-sandboxed, so the dialog is in-process.) I initiated the modal dialog from a task submitted to the main dispatch queue. It turns out that the internal implementation of the open dialog also dispatches some work to the main queue asynchronously. Since the main dispatch queue was occupied by my task which was running the dialog, it didn't process the framework's tasks until after the dialog completed. The symptom was that the dialog would fail to show the files until some internal timeout had expired, which was on the order of a minute or so.

Note that this wasn't a case of deadlock caused by a synchronous request to the main queue from the main thread, although that can also happen. With GCD, such a synchronous request is certain to deadlock. With -performSelectorOnMainThread:..., it won't because a synchronous request (waitUntilDone set to YES) is just run directly.

By the way, you say "the first code is asynchronous" as if to contrast with the second code. Both are asynchronous, since you passed NO for waitUntilDone in the second.


Update:

Consider code like this:

dispatch_async(dispatch_get_main_queue(), ^{
    printf("outer task, milestone 1\n");
    dispatch_async(dispatch_get_main_queue(), ^{
        printf("inner task\n");
    });
    // Although running the run loop directly like this is uncommon, this simulates what
    // happens if you do something like run a modal dialog or call -[NSTask waitUntilExit].
    [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
    printf("outer task, milestone 2\n");
});

This will log:

outer task, milestone 1
outer task, milestone 2
inner task

The inner task won't get to run until the outer task has completed. That's true even though the outer task ran the main run loop, which is what processes tasks dispatched to the main queue. The reason is that the main queue is a serial queue and will never start a new task while it's still running a task.

If you change the inner dispatch_async() to dispatch_sync(), then the program will deadlock.

By contrast, consider:

- (void) task2
{
    printf("task2\n");
}

- (void) task1
{
    printf("task1 milestone 1\n");
    [self performSelectorOnMainThread:@selector(task2) withObject:nil waitUntilDone:NO];
    [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
    printf("task1 milestone 2\n");
}

(... in some other method:)
    [self performSelectorOnMainThread:@selector(task1) withObject:nil waitUntilDone:NO];

That will log:

task1 milestone 1
task2
task1 milestone 2

Running the run loop inside of -task1 gives an opportunity for the inner -performSelectorOnMainThread:... to run. This is the big difference between the two techniques.

If you change the NO to YES in -task1, this still works without deadlock. That's another difference. That's because, when -performSelectorOnMainThread:... is called with waitUntilDone set as true, it checks if it's called on the main thread. If it is, then it just directly invokes the selector right there. It's as though it were just a call to -performSelector:withObject:.

Upvotes: 20

Dave DeLong
Dave DeLong

Reputation: 243146

Yes, there appears to be a small difference. Let's write some code and see what it is:

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
    NSLog(@"Starting!");

    CFRunLoopObserverRef o1 = CFRunLoopObserverCreateWithHandler(NULL, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        NSLog(@"runloop phase: %@", NSStringFromRunLoopActivity(activity));
    });
    CFRunLoopAddObserver(CFRunLoopGetMain(), o1, kCFRunLoopDefaultMode);

    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"dispatch_async 1");
    });
    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"dispatch_async 2");
    });

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

    /*
    NSLog(@"Reentering");
    [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.25]];
    NSLog(@"Reexiting");
    */

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        CFRunLoopRemoveObserver(CFRunLoopGetMain(), o1, kCFRunLoopDefaultMode);
        CFRelease(o1);
    });
}

- (void)log {
    NSLog(@"performSelector");
}

In this bit of code, we're going to set up a RunLoop Observer to inject some logging while the runloop spins. This will help us understand when our asynchronous code is getting executed. (the NSStringFromRunLoopActivity() function is a custom function to simply turn the activity value into a string; its implementation is uninteresting)

We're going to dispatch two things to the main queue, and we're going to dispatch two log selectors to the main thread. Notice that we're dispatch_async-ing before the -performSelector: call.

Then we're going to do some teardown of the observer so we don't get log spew.

When we run this, we see:

2014-05-25 07:57:26.054 EmptyAppKit[35437:303] Starting!
2014-05-25 07:57:26.055 EmptyAppKit[35437:303] runloop phase: Entry
2014-05-25 07:57:26.055 EmptyAppKit[35437:303] runloop phase: BeforeTimers
2014-05-25 07:57:26.055 EmptyAppKit[35437:303] runloop phase: BeforeSources
2014-05-25 07:57:26.056 EmptyAppKit[35437:303] performSelector
2014-05-25 07:57:26.056 EmptyAppKit[35437:303] performSelector
2014-05-25 07:57:26.056 EmptyAppKit[35437:303] runloop phase: Exit
2014-05-25 07:57:26.056 EmptyAppKit[35437:303] runloop phase: Entry
2014-05-25 07:57:26.056 EmptyAppKit[35437:303] runloop phase: BeforeTimers
2014-05-25 07:57:26.056 EmptyAppKit[35437:303] runloop phase: BeforeSources
2014-05-25 07:57:26.056 EmptyAppKit[35437:303] dispatch_async 1
2014-05-25 07:57:26.056 EmptyAppKit[35437:303] dispatch_async 2
2014-05-25 07:57:26.057 EmptyAppKit[35437:303] runloop phase: Exit
2014-05-25 07:57:26.057 EmptyAppKit[35437:303] runloop phase: Entry
2014-05-25 07:57:26.057 EmptyAppKit[35437:303] runloop phase: BeforeTimers
2014-05-25 07:57:26.057 EmptyAppKit[35437:303] runloop phase: BeforeSources
2014-05-25 07:57:26.067 EmptyAppKit[35437:303] runloop phase: BeforeWaiting
2014-05-25 07:57:26.068 EmptyAppKit[35437:303] runloop phase: AfterWaiting
2014-05-25 07:57:26.068 EmptyAppKit[35437:303] runloop phase: Exit
...

From this I see a couple of things:

  1. Run Loops exit as soon as they've found something to do. Notice how in both the performSelector and dispatch_async cases, the run loop gets to checking sources, but never gets to the "BeforeWaiting" stage like it does afterwards.
  2. Run Loops do as much of a single thing as they can. In both cases, the run loop executes both performSelectors and both dispatch_asyncs.
  3. Run Loops prefer performing selectors overs dispatching blocks. Remember how we dispatched before performing the selector? Yet the selector was performed before the blocks were executed. I'm guessing that whatever mechanism the run loop has for doing this executes selector performance at a higher priority (or simply earlier).
  4. Re-entrancy doesn't change this. If you uncomment the [[NSRunLoop mainRunLoop] runUntilDate...] code, the order of things does not change, and both blocks and selectors are performed up re-entering the run loop.

Upvotes: 7

Related Questions