主队列上的 dispatch_sync 与 dispatch_async

Posted

技术标签:

【中文标题】主队列上的 dispatch_sync 与 dispatch_async【英文标题】:dispatch_sync vs. dispatch_async on main queue 【发布时间】:2011-09-26 04:40:02 【问题描述】:

请耐心等待,这需要一些解释。我有一个类似于下面的函数。

上下文:“aProject”是一个名为 LPProject 的核心数据实体,带有一个名为“memberFiles”的数组,其中包含另一个名为 LPFile 的核心数据实体的实例。每个 LPFile 代表磁盘上的一个文件,我们要做的是打开每个文件并解析其文本,寻找指向其他文件的 @import 语句。如果我们找到@import 语句,我们希望找到它们指向的文件,然后通过添加与代表第一个文件的核心数据实体的关系,将该文件“链接”到该文件。由于所有这些都可能在大文件上花费一些时间,因此我们将使用 GCD 在主线程之外完成。

- (void) establishImportLinksForFilesInProject:(LPProject *)aProject 
    dispatch_queue_t taskQ = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
     for (LPFile *fileToCheck in aProject.memberFiles) 
         if (//Some condition is met) 
            dispatch_async(taskQ, ^
                // Here, we do the scanning for @import statements. 
                // When we find a valid one, we put the whole path to the imported file into an array called 'verifiedImports'. 

                // go back to the main thread and update the model (Core Data is not thread-safe.)
                dispatch_sync(dispatch_get_main_queue(), ^

                    NSLog(@"Got to main thread.");

                    for (NSString *import in verifiedImports)   
                            // Add the relationship to Core Data LPFile entity.
                    
                );//end block
            );//end block
        
    

现在,事情变得奇怪了:

此代码有效,但我发现了一个奇怪的问题。如果我在一个有几个文件(大约 20 个)的 LPProject 上运行它,它运行得很好。但是,如果我在具有更多文件(例如 60-70)的 LPProject 上运行它,它确实 NOT 正确运行。我们永远不会回到主线程,NSLog(@"got to main thread"); 永远不会出现并且应用程序挂起。但是,(这就是事情变得非常奇怪的地方)---如果我首先在小项目上运行代码,然后在大项目上运行它,一切都会完美运行。只有当我首先在大型项目上运行代码时才会出现问题。

如果我将第二条调度线更改为以下内容,那就是踢球者:

dispatch_async(dispatch_get_main_queue(), ^

(也就是说,使用async 而不是sync 将块分派到主队列),一切正常。完美。无论项目中有多少文件!

我无法解释这种行为。任何有关下一步测试的帮助或提示将不胜感激。

【问题讨论】:

注意:为简洁起见,我已经编辑了“扫描”和“核心数据输入”代码片段。但是,我几乎可以肯定它们不是罪魁祸首,因为如果我将所有内容都放在一个线程上,它们就可以完美运行,并且它们在上述多线程情况下也可以完美运行(通过首先运行一个小项目来“预热”一切)和/或在主队列上使用 dispatch_async() 而不是 dispatch_sync())。 听起来你遇到了死锁问题 您应该在应用程序处于这种状态时运行样本或仪器,以查看其他线程都在做什么。如果他们陷入僵局,发生的事情应该会更加明显。 在哪里调用 NSManagedObjectContext -save?您是否有该通知的观察者强制它使用 performSelectorOnMainThread 响应主线程? 应编辑此问题以指示单个文件 I/O 发生的位置与 CoreData 查询发生的位置。就目前而言,它具有误导性。 【参考方案1】:

这是与磁盘 I/O 和 GCD 相关的常见问题。基本上,GCD 可能会为每个文件生成一个线程,并且在某个时间点,您有太多线程让系统无法在合理的时间内提供服务。

每次调用 dispatch_async() 并在该块中尝试任何 I/O(例如,看起来您正在此处读取一些文件),很可能该代码块所在的线程是执行将在等待从文件系统读取数据时阻塞(被操作系统暂停)。 GCD 的工作方式是,当它看到它的一个工作线程在 I/O 上被阻塞并且你仍然要求它同时做更多的工作时,它只会产生一个新的工作线程。因此,如果您尝试在一个并发队列上打开 50 个文件,您最终可能会导致 GCD 产生约 50 个线程。

对于系统而言,线程过多,无法进行有意义的服务,最终导致主线程无法获得 CPU。

解决此问题的方法是使用串行队列而不是并发队列来执行基于文件的操作。这很容易做到。您需要创建一个串行队列并将其作为 ivar 存储在您的对象中,这样您就不会最终创建多个串行队列。所以删除这个调用:

dispatch_queue_t taskQ = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

在你的 init 方法中添加这个:

taskQ = dispatch_queue_create("com.yourcompany.yourMeaningfulLabel", DISPATCH_QUEUE_SERIAL);

在你的 dealloc 方法中添加这个:

dispatch_release(taskQ);

并将其作为 ivar 添加到您的类声明中:

dispatch_queue_t taskQ;

【讨论】:

Mike Ash 也有一篇关于这个问题的优秀文章:mikeash.com/pyblog/friday-qa-2009-09-25-gcd-practicum.html @Ryan - 感谢您的意见。我也遇到过这种情况,但如果问题是并发线程太多,我们预计大型项目每次都会失败。在这种情况下,只要我先在一个较小的项目上运行代码,它就可以工作。 (注意两个项目是完全独立的文件,所以没有缓存文件等) 如果启用自动引用计数会怎样?【参考方案2】:

我相信 Ryan 走在正确的道路上:当一个项目有 1,500 个文件(我决定测试的数量)时,就会产生太多线程。

所以,我将上面的代码重构为这样:

- (void) establishImportLinksForFilesInProject:(LPProject *)aProject

        dispatch_queue_t taskQ = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

     dispatch_async(taskQ, 
     ^

     // Create a new Core Data Context on this thread using the same persistent data store    
     // as the main thread. Pass the objectID of aProject to access the managedObject
     // for that project on this thread's context:

     NSManagedObjectID *projectID = [aProject objectID];

     for (LPFile *fileToCheck in [backgroundContext objectWithID:projectID] memberFiles])
     
        if (//Some condition is met)
        
                // Here, we do the scanning for @import statements. 
                // When we find a valid one, we put the whole path to the 
                // imported file into an array called 'verifiedImports'. 

                // Pass this ID to main thread in dispatch call below to access the same
                // file in the main thread's context
                NSManagedObjectID *fileID = [fileToCheck objectID];


                // go back to the main thread and update the model 
                // (Core Data is not thread-safe.)
                dispatch_async(dispatch_get_main_queue(), 
                ^
                    for (NSString *import in verifiedImports)
                      
                       LPFile *targetFile = [mainContext objectWithID:fileID];
                       // Add the relationship to targetFile. 
                    
                 );//end block
         
    
    // Easy way to tell when we're done processing all files.
    // Could add a dispatch_async(main_queue) call here to do something like UI updates, etc

    );//end block
    

所以,基本上,我们现在生成一个读取所有文件的线程,而不是每个文件一个线程。此外,事实证明,在 main_queue 上调用 dispatch_async() 是正确的方法:工作线程会将该块分派到主线程,而不是等待它返回,然后再继续扫描下一个文件。

这个实现基本上按照 Ryan 的建议设置了一个“串行”队列(for 循环是它的串行部分),但有一个优点:当 for 循环结束时,我们完成了所有文件的处理,我们可以只需在此处粘贴一个 dispatch_async(main_queue) 块即可执行我们想要的任何操作。这是一种很好的方式来判断并发处理任务何时完成并且在我的旧版本中不存在。

这里的缺点是在多个线程上使用 Core Data 有点复杂。但这种方法对于有 5,000 个文件的项目(这是我测试过的最高值)似乎是万无一失的。

【讨论】:

是的,您最初的问题并没有说明您的“文件”确实是 CoreData 对象。这是一个完全不同类型的问题。我的回复涉及实际的文件 I/O。在这一点上,我意识到如果没有完整的源代码清单,我实际上无法判断您在做什么。我不确定您何时进行文件 I/O 或从 CoreData 读取数据。如果您想了解更多信息,请随时列出您正在做的事情的来源。【参考方案3】:

我认为用图表更容易理解:

对于作者描述的情况:

|任务Q| ***********开始|

|dispatch_1 ***********|---------

|dispatch_2 *************|---------

.

|dispatch_n ***************************|---------

|main queue(sync)|**开始派发到main|

******************************|--dispatch_1--|--dispatch_2--|--dispatch3--|*** ******************************|--dispatch_n|,

这使得同步主队列如此繁忙,最终导致任务失败。

【讨论】:

以上是关于主队列上的 dispatch_sync 与 dispatch_async的主要内容,如果未能解决你的问题,请参考以下文章

从主队列调用 dispatch_sync 并且执行的块保存到核心数据时 iOS 死锁

为啥我们不能在当前队列上使用 dispatch_sync?

dispatch_sync()

Dispatch Queues调度队列

从并发队列中调用 dispatch_sync - 它是不是完全阻塞?

我在哪个 GCD 队列上运行,无论是不是是主队列?