主队列上的dispatch_sync vs. dispatch_async

53

请耐心听我解释一下。我有一个函数,如下所示。

背景信息: "aProject"是一个名为LPProject的Core Data实体,其中包含一个名为'memberFiles'的数组,该数组包含另一个Core Data实体LPFile的实例。每个LPFile代表磁盘上的一个文件,我们想要做的是打开每个文件并解析其文本,查找指向其他文件的@import语句。如果我们找到了@import语句,我们希望定位它们指向的文件,然后通过将关系添加到表示第一个文件的Core Data实体来将该文件与此文件“链接”起来。由于所有这些都可能需要花费大量时间在大型文件上完成,因此我们将使用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上运行它,它就不能正确地运行。我们从未回到主线程,NSLog(@"got to main thread");从未出现,应用程序挂起。但是,(这就是事情变得非常奇怪的地方)---如果我先在小项目上运行代码,然后再在大项目上运行它,一切都可以完美地工作。只有当我首先在大项目上运行代码时,才会出现问题。

这里的关键是,如果我将第二个分派行更改为以下内容:

dispatch_async(dispatch_get_main_queue(), ^{

也就是说,使用async而不是sync将块派发到主队列,无论项目中有多少个文件,都可以完美地工作。

我无法解释这种行为。希望您能提供帮助或提示下一步要测试什么。


注意:为了简洁起见,我已经删除了“扫描”和“Core Data输入”代码片段。但我几乎可以确定它们不是罪魁祸首,因为如果我把所有东西都放在一个线程上,它们就能完美地工作,并且在上述多线程情况下(通过先运行一个小项目来“热身”或者使用dispatch_async()在主队列上而不是dispatch_sync()),它们也能完美地工作。 - Bryan
1
听起来你遇到了死锁问题。 - Dave DeLong
当应用程序处于这种状态时,您应该运行示例或工具来查看其他线程正在做什么。如果它们被死锁了,那么发生的情况应该更加明显。 - Jon Hess
NSManagedObjectContext -save被调用在哪里?你有一个观察者来强制使用performSelectorOnMainThread将其响应转发到主线程吗? - ImHuntingWabbits
1
这个问题应该被编辑以指明单独的文件I/O发生在哪里,而CoreData查询发生在哪里。目前来看,它是具有误导性的。 - Ryan
@wabbits:在我进入这个方法之前已经调用了-save。在我们在后台线程创建新上下文之前将主线程的managedObjectContext中的任何更改推送到持久存储中,意味着后台上下文是主线程上下文的精确副本。我从不在后台上下文中更改任何managedObjects,因此我永远不需要将任何更改推回到主线程的上下文中。我只需返回主线程并对该上下文进行更改即可。 - Bryan
3个回答

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); 在你的初始化方法中添加以下内容: taskQ = dispatch_queue_create("com.yourcompany.yourMeaningfulLabel", DISPATCH_QUEUE_SERIAL); 在你的dealloc方法中添加以下内容: dispatch_release(taskQ); 并在类声明中添加以下内容作为ivar: dispatch_queue_t taskQ;

6
Mike Ash也有一个关于这个问题的很好的讲解:http://mikeash.com/pyblog/friday-qa-2009-09-25-gcd-practicum.html - jscs
2
@Ryan - 感谢您的意见。我也考虑过这个问题,但如果问题是并发线程太多,我们预计每次都会导致大型项目失败。但是在这种情况下,只要我先在较小的项目上运行代码,它就可以正常工作。(请注意,这两个项目是完全独立的文件,因此不会有任何缓存等问题。) - Bryan
1
如果启用自动引用计数会怎样? - byJeevan

5

我认为Ryan走在正确的道路上:当项目有1500个文件(这是我决定测试的数量)时,产生的线程太多了。

因此,我重构了上面的代码,使其像这样工作:

- (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
    }

因此,基本上我们现在生成一个线程来读取所有文件,而不是每个文件都有一个线程。此外,调用dispatch_async()在main_queue上是正确的方法:工作线程将将该块分派到主线程,并在继续扫描下一个文件之前不等待其返回。
这种实现基本上设置了“串行”队列,就像Ryan建议的那样(for循环是其中的串行部分),但是具有一个优点:当for循环结束时,我们已经处理完所有文件,我们可以只需在那里放置一个dispatch_async(main_queue)块来做任何想要做的事情。这是一种非常好的方式,以告知并发处理任务何时完成,而在我的旧版本中不存在这种方式。
这里的缺点是在多个线程上使用Core Data有点更加复杂。但是这种方法似乎对于拥有5,000个文件的项目是牢不可破的(这是我测试过的最高值)。

1
是的,你最初的问题没有明确表明你的“文件”实际上是CoreData对象。这是完全不同类型的问题。我的回答涉及实际的文件I/O。此时我意识到如果没有看到完整的源代码清单,我无法确定你正在做什么。我不确定你何时进行文件I/O或从CoreData读取数据。如果您想要更多的输入,请随时列出您正在做的内容的源代码。 - Ryan

0

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

对于作者描述的情况:

|taskQ| ***********start|

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

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

.

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

|主队列(同步)|**开始分派到主线程|

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

这使得同步的主队列非常繁忙,最终无法完成任务。


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