"什么是GCD主队列和主线程的区别?" GCD主队列和主线程有什么不同之处?

11
我在 Stack Overflow 上读到一条评论,说将队列分派到主线程并不等同于在主线程上执行代码。如果我理解正确的话,用户是在说这个:
dispatch_async(dispatch_get_main_queue(),
                 ^{
                      // some code
                 });

不是同一个

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

- (void) doStuff {
  // some code
}

这个评论是否有一些真实性?

除了第一段代码是异步的这一事实以外,对我来说,两段代码在主线程上执行的方式是相同的。它们之间是否存在技术差异呢?

我之所以问这个问题,是因为我之前有一段使用dispatch_async在主线程上更新UI的代码没有生效,但当我改用performSelectorOnMainThread的第二种形式时,它却有效了。

2个回答

20
是的,它们有区别。主调度队列是一个串行队列。这意味着,当它正在运行已提交的任务时,它不能运行任何其他任务。即使它运行内部事件循环,这也是正确的。
-performSelectorOnMainThread:... 通过运行循环源进行操作。运行循环源可以在内部运行循环中触发,即使该内部运行循环是由之前对同一源的触发引起的。
其中一个情况是运行模态文件打开对话框。 (非沙盒,因此对话框在进程中。)我从提交给主调度队列的任务启动了模态对话框。事实证明,打开对话框的内部实现也异步地将一些工作分派到主队列。由于主调度队列被运行对话框的任务占用,直到对话框完成后才处理框架的任务。症状是对话框无法显示文件,直到某个内部超时已过期,这大约需要一分钟左右。
请注意,这不是由主线程对主队列发出的同步请求导致死锁的情况,尽管这也可能发生。使用GCD,这样的同步请求肯定会死锁。使用-performSelectorOnMainThread:...,它不会死锁,因为同步请求(waitUntilDone设置为YES)只是直接运行。
顺便说一句,您说“第一个代码是异步的”,好像在与第二个代码形成对比。由于在第二个代码中传递了NO参数以表示不等待执行结果,因此两者都是异步的。
更新:
考虑这样的代码:
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");
});

这将被记录:

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

在外部任务完成之前,内部任务不会运行。即使外部任务运行了主运行循环,也就是处理分派到主队列的任务,这一点仍然是正确的。原因是主队列是串行队列,在它还在运行任务时永远不会启动新任务。

如果将内部的dispatch_async()更改为dispatch_sync(),那么程序将会死锁。

相比之下,请考虑:

- (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];

那将记录:

task1 milestone 1
task2
task1 milestone 2

-task1中运行运行循环,为内部的-performSelectorOnMainThread:...提供了运行的机会。这是两种技术之间的重大区别。

如果您在-task1中将NO更改为YES,这仍然可以避免死锁。那是另一个区别。那是因为当使用waitUntilDone设置为true调用-performSelectorOnMainThread:...时,它会检查是否在主线程上调用。如果是,则直接在那里调用选择器。就像调用-performSelector:withObject:一样。


-performSelectorOnMainThread:... 通过运行循环源操作。即使该内部运行循环是由先前对同一源的触发而导致的,运行循环源也可以在内部运行循环中触发。 真是什么鬼?这句话简直无法理解。 - Duck
这并非不可能,你只需要了解一些关于运行循环和运行循环源的知识。请参见https://developer.apple.com/library/ios/documentation/cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html。 - Ken Thomases
谢谢。解释得非常好。 - Duck

7

是的,看起来有一点差别。让我们写些代码来看看它是什么:

- (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");
}

在这段代码中,我们将设置一个RunLoop Observer,以注入一些日志记录,同时运行循环。这将帮助我们了解异步代码何时被执行。(NSStringFromRunLoopActivity()函数是一个自定义函数,用于将activity值简单地转换为字符串;其实现并不重要)
我们将向主队列调度两个内容,并将两个log选择器调度到主线程。请注意,在调用-performSelector:之前,我们正在进行dispatch_async
然后,我们将拆卸观察器,以避免日志输出。
当我们运行此代码时,我们会看到:
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
...

从这里我看到了几个事情:

  1. 运行循环在找到要执行的任务后就会退出。注意,在performSelectordispatch_async两种情况下,运行循环会检查源,但从未像之后那样达到“BeforeWaiting”阶段。
  2. 运行循环尽可能做一件事。在这两种情况下,运行循环都会执行所有performSelectors和两个dispatch_asyncs
  3. 运行循环更喜欢执行选择器而不是调度块。还记得我们在执行选择器之前进行了调度吗?然而,选择器在块执行之前被执行。我猜测运行循环用于执行此操作的任何机制都会以更高优先级(或更早)的方式执行选择器性能。
  4. 可重入性并不会改变这一点。如果取消注释[[NSRunLoop mainRunLoop] runUntilDate...]代码,则顺序不会改变,并且在重新进入运行循环之前会执行块和选择器。

我认为这已经陷入了细节之中,忽略了更大的差异。虽然就这个例子而言,#4是正确的,但在重新进入运行循环方面,通常会对这两种技术产生很大的影响。 - Ken Thomases

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接