dispatch_sync与主队列上的dispatch_async

时间:2011-06-30 17:31:25

标签: objective-c cocoa core-data 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
        }
    }
}

现在,事情变得奇怪了:

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

如果我将第二个调度线更改为:

,这就是踢球者
dispatch_async(dispatch_get_main_queue(), ^{

(也就是说,使用async代替sync将块分派给主队列),一切都在运行。完美。无论项目中的文件数量多少!

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

3 个答案:

答案 0 :(得分: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;

答案 1 :(得分: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
    }

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

这个实现本质上建立了一个像Ryan建议的“串行”队列(for循环是它的串行部分),但有一个好处:当for循环结束时,我们已经完成了处理所有文件,我们可以只需在那里粘贴一个dispatch_async(main_queue)块来做任何我们想做的事情。这是一种非常好的方式来判断并发处理任务何时完成,并且在旧版本中不存在。

这里的缺点是在多个线程上使用Core Data会有点复杂。但对于拥有5,000个文件的项目(这是我测试过的最高版本),这种方法似乎是防弹的。

答案 2 :(得分:0)

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

对于作者描述的情况:

| TASKQ | ***********启动|

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

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

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

|主队列(sync)| **开始发送到main |

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

使同步主队列忙碌,最终导致任务失败。