GCD主队列和主线程有什么区别?

Spa*_*Dog 10 cocoa multithreading grand-central-dispatch

我读了关于SO的注释,将队列调度到主线程与在主线程上执行代码不同.如果我理解正确,用户就是这么说的

dispatch_async(dispatch_get_main_queue(),
                 ^{
                      // some code
                 });
Run Code Online (Sandbox Code Playgroud)

与此不一样

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

- (void) doStuff {
  // some code
}
Run Code Online (Sandbox Code Playgroud)

这个评论有一些真实的吗?

排除第一个代码是异步的事实,对我来说,两个代码在主线程上都是相同的.它们之间有什么技术差异吗?

我问的是因为我有一些代码在主线程上使用dispatch_async更新UI并且它无法工作但是当我使用performSelectorOnMainThread将其更改为第二个表单时,它工作正常.

Ken*_*ses 18

是的,有区别.主调度队列是一个串行队列.这意味着,虽然它正在运行已提交给它的任务,但它无法运行任何其他任务.即使它运行内部事件循环也是如此.

-performSelectorOnMainThread:...通过运行循环源运行.运行循环源可以在内部运行循环中触发,即使该内部运行循环是先前触发该相同源的结果.

这个位于我的一个案例是运行模态文件打开对话框.(非沙盒,因此对话框正在进行中.)我从提交到主调度队列的任务启动了模式对话框.事实证明,打开对话框的内部实现也会异步地将一些工作分配给主队列.由于主调度队列被运行对话框的任务占用,因此在对话框完成之前,它不会处理框架的任务.症状是对话框无法显示文件,直到某些内部超时已到期,大约一分钟左右.

请注意,这不是由主线程对主队列的同步请求引起的死锁情况,尽管这也可能发生.使用GCD,这样的同步请求肯定会死锁.有-performSelectorOnMainThread:...,它不会,因为同步请求(waitUntilDone设置为YES)只是直接运行.

顺便说一句,你说"第一个代码是异步的",好像要与第二个代码形成对比.两者都是异步的,因为你在第二个传递NOwaitUntilDone.


更新:

考虑这样的代码:

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");
});
Run Code Online (Sandbox Code Playgroud)

这将记录:

outer task, milestone 1
outer task, milestone 2
inner task
Run Code Online (Sandbox Code Playgroud)

在外部任务完成之前,内部任务不会运行.即使外部任务运行主运行循环也是如此,这是处理分派到主队列的任务的过程.原因是主队列是一个串行队列,在它仍在运行任务时永远不会启动新任务.

如果将内部更改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];
Run Code Online (Sandbox Code Playgroud)

那会记录:

task1 milestone 1
task2
task1 milestone 2
Run Code Online (Sandbox Code Playgroud)

在内部运行run循环为内部运行-task1提供了机会-performSelectorOnMainThread:....这是两种技术之间的巨大差异.

如果更改NOYESin -task1,这仍然可以在没有死锁的情况下运行.这是另一个不同之处.这是因为,当-performSelectorOnMainThread:...使用waitUntilDoneset为true 调用时,它会检查是否在主线程上调用它.如果是,那么它就直接在那里调用选择器.就好像它只是一个电话-performSelector:withObject:.


Dav*_*ong 6

是的,似乎有一点不同.让我们编写一些代码并查看它是什么:

- (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");
}
Run Code Online (Sandbox Code Playgroud)

在这段代码中,我们将设置一个RunLoop Observer来在runloop旋转时注入一些日志记录.这将有助于我们了解异步代码何时执行.(该NSStringFromRunLoopActivity()函数是一个自定义函数,只需将activity值转换为字符串;其实现无趣)

我们将向主队列发送两个东西,我们将把两个log选择器分派给主线程.请注意,我们正在通话dispatch_async之前-performSelector:.

然后我们要对观察者进行一些拆解,这样我们就不会得到日志.

当我们运行时,我们看到:

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
...
Run Code Online (Sandbox Code Playgroud)

从这里我看到了几件事:

  1. 一旦他们发现了某些事情,就立即退出循环.请注意,在这两个performSelectordispatch_async的情况下,运行循环到达检查来源,但像它以后不会继续执行"BeforeWaiting"阶段.
  2. Run Loops尽可能地做一件事.在这两种情况下,运行循环都执行两者 performSelectors和两者dispatch_asyncs.
  3. Run Loops更喜欢在调度块上执行选择器.还记得在执行选择器之前我们如何调度?然而,选择器是在块执行之前执行的.我猜测运行循环用于执行此操作的任何机制都会以更高的优先级(或简单地更早)执行选择器性能.
  4. 重新入侵不会改变这一点.如果取消注释[[NSRunLoop mainRunLoop] runUntilDate...]代码,事物的顺序不会改变,并且执行重新进入运行循环的块和选择器都会执行.