dispatch_sync与主队列上的dispatch_async

Bry*_*yan 52 cocoa core-data objective-c grand-central-dispatch objective-c-blocks

跟我说,这需要一些解释.我有一个看起来像下面的功能.

上下文:"aProject"是名为LPProject的核心数据实体,其名为"memberFiles"的数组包含另一个名为LPFile的Core Data实体的实例.每个LPFile表示磁盘上的文件,我们要做的是打开每个文件并解析其文本,查找指向OTHER文件的@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
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

现在,这里的事情变得奇怪了:

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

如果我将第二个调度线更改为此,那么这就是踢球者:

dispatch_async(dispatch_get_main_queue(), ^{
Run Code Online (Sandbox Code Playgroud)

(也就是说,使用async而不是sync将块分派给主队列),一切都在工作.完美.无论项目中的文件数量多少!

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

Rya*_*yan 53

这是与磁盘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也有一篇关于这个问题的优秀文章:http://mikeash.com/pyblog/friday-qa-2009-09-25-gcd-practicum.html (6认同)
  • @Ryan - 感谢您的投入.这也发生在我身上,但如果问题是并发线程太多,我们会期望大型项目每次都会失败.在这种情况下,只要我首先在较小的项目上运行代码,它就可以工作.(请注意,这两个项目是完全独立的文件,因此没有缓存文件等) (2认同)

Bry*_*yan 5

我相信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
    }
Run Code Online (Sandbox Code Playgroud)

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

这个实现基本上建立了一个"串行"队列,正如Ryan建议的那样(for循环是它的串行部分),但有一个优点:当for循环结束时,我们已经完成了处理所有文件,我们可以坚持dispatch_async(main_queue)阻止那里做我们想做的事.这是一种非常好的方式来判断并发处理任务何时完成并且在旧版本中不存在.

这里的缺点是在多线程上使用Core Data会有点复杂.但对于拥有5,000个文件的项目(这是我测试过的最高级别),这种方法似乎是无懈可击的.