45

我有一个 NSManagedObjectContext 声明如下:

- (NSManagedObjectContext *) backgroundMOC {
    if (backgroundMOC != nil) {
        return backgroundMOC;
    }
    backgroundMOC = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    return backgroundMOC;
}

请注意,它是使用私有队列并发类型声明的,因此它的任务应该在后台线程上运行。我有以下代码:

-(void)testThreading
{
    /* ok */
    [self.backgroundMOC performBlock:^{
        assert(![NSThread isMainThread]); 
    }];

    /* CRASH */
    [self.backgroundMOC performBlockAndWait:^{
        assert(![NSThread isMainThread]); 
    }];
}

为什么调用performBlockAndWait在主线程而不是后台线程上执行任务?

4

4 回答 4

103

折腾另一个答案,试图解释为什么performBlockAndWait总是在调用线程中运行。

performBlock是完全异步的。它总是将块排入接收 MOC 的队列,然后立即返回。因此,

[moc performBlock:^{
    // Foo
}];
[moc performBlock:^{
    // Bar
}];

将在 moc 队列中放置两个块。它们将始终异步执行。一些未知线程将从队列中拉出块并执行它们。此外,这些块被包装在它们自己的自动释放池中,它们也将代表一个完整的 Core Data 用户事件 ( processPendingChanges)。

performBlockAndWait不使用内部队列。它是在调用线程的上下文中执行的同步操作。当然,它会等到队列上的当前操作执行完毕,然后该块将在调用线程中执行。这已记录在案(并在多个 WWDC 演示文稿中重申)。

此外,performBockAndWait它是可重入的,因此嵌套调用都发生在该调用线程中。

Core Data 工程师已经非常清楚,基于队列的 MOC 操作运行的实际线程并不重要。关键是使用performBlock*API 进行同步。

因此,将'performBlock'视为“此块被放置在队列中,将在某个未确定的时间,在某个未确定的线程中执行。该函数将在入队后立即返回给调用者”

performBlockAndWait是“这个块将在某个不确定的时间,在这个完全相同的线程中执行。该函数将在此代码完全执行后返回(这将在与此 MOC 关联的当前队列耗尽后发生)。”

编辑

您确定“performBlockAndWait 不使用内部队列”吗?我认为确实如此。唯一的区别是 performBlockAndWait 将等到块完成。调用线程是什么意思?据我了解,[moc performBlockAndWait] 和 [moc performBloc] 都在其私有队列(后台或主)上运行。这里的重要概念是 moc 拥有队列,而不是相反。如果我错了,请纠正我。– 菲利普007

不幸的是,我按照自己的方式表达了答案,因为就其本身而言,它是不正确的。但是,在原始问题的上下文中,它是正确的。具体来说,当调用performBlockAndWait私有队列时,该块将在调用该函数的线程上执行——它不会被放入队列并在“私有线程”上执行。

现在,在我深入细节之前,我想强调一下,依赖库的内部工作是非常危险的。您真正应该关心的是,您永远不能期望特定线程执行块,除非与主线程相关联。因此,不建议期望 a不在performBlockAndWait主线程上执行,因为它将在调用它的线程上执行。

performBlockAndWait使用 GCD,但它也有自己的层(例如,防止死锁)。如果您查看 GCD 代码(它是开源的),您可以看到同步调用是如何工作的 - 通常它们与队列同步并调用调用函数的线程上的块 - 除非队列是主队列或一个全局队列。此外,在 WWDC 会谈中,Core Data 工程师强调performBlockAndWait将在调用线程中运行的点。

所以,当我说它不使用内部队列时,并不意味着它根本不使用数据结构。它必须将调用与队列中已经存在的块以及在其他线程和其他异步调用中提交的块同步。但是,当调用performBlockAndWait它时,它不会将块放在队列中......相反,它会同步访问并在调用该函数的线程上运行提交的块。

现在,SO 不是一个很好的论坛,因为它比这更复杂一些,尤其是主队列和 GCD 全局队列——但后者对 Core Data 并不重要。

要点是,当您调用任何performBlock*或 GCD 函数时,您不应期望它在任何特定线程上运行(与主线程相关的东西除外),因为队列不是线程,只有主队列会在特定线程上运行块线。

调用核心数据时performBlockAndWait,该块将在调用线程中执行(但将与提交到队列的所有内容适当同步)。

我希望这是有道理的,尽管它可能只会引起更多的混乱。

编辑

此外,您可以看到这种不言而喻的影响,因为performBlockAndWait提供可重入支持的方式破坏了块的 FIFO 排序。举个例子...

[context performBlockAndWait:^{
    NSLog(@"One");
    [context performBlock:^{
        NSLog(@"Two");
    }];
    [context performBlockAndWait:^{
        NSLog(@"Three");
    }];
}];

请注意,严格遵守队列的 FIFO 保证意味着嵌套performBlockAndWait(“三”)将在异步块(“二”)之后运行,因为它是在提交异步块之后提交的。dispatch_sync然而,这不是发生的事情,因为这是不可能的......出于同样的原因,嵌套调用会导致死锁。如果使用同步版本,请注意一些事项。

通常,尽可能避免使用同步版本,因为这dispatch_sync可能会导致死锁,并且任何可重入版本performBlockAndWait都必须做出一些“糟糕”的决定来支持它......就像让同步版本“跳入”队列一样。

于 2012-08-06T17:36:13.127 回答
3

为什么不?Grand Central Dispatch 的块并发范式(我假设 MOC 在内部使用)被设计为只有运行时和操作系统需要担心线程,而不是开发人员(因为操作系统可以做得比你可以做的更好)拥有更详细的信息)。太多人认为队列与线程相同。他们不是。

排队的块不需要在任何给定的线程上运行(主队列中的块必须在主线程上执行)。因此,事实上,如果运行时认为它比为其创建线程更有效,有时同步(即 performBlockAndWait)排队块将在主线程上运行。由于无论如何您都在等待结果,因此如果主线程在操作期间挂起,它不会改变程序的运行方式。

最后一部分我不确定我是否记错了,但在关于 GCD 的 WWDC 2011 视频中,我相信有人提到运行时将尽可能在主线程上运行以进行同步操作,因为它是更高效。最后,我想“为什么”的答案只能由设计系统的人来回答。

于 2012-08-06T16:52:16.163 回答
0

我不认为 MOC 有义务使用后台线程。performBlock:如果您使用or performBlockAndWait:,它只是有义务确保您的代码不会遇到 MOC 的并发问题 由于performBlockAndWait:应该阻塞当前线程,因此在该线程上运行该块似乎是合理的。

于 2012-08-06T16:10:19.410 回答
0

performBlockAndWait:调用仅确保您以不引入并发的方式执行代码(即在 2 个线程上performBlockAndWait:不会同时运行,它们将相互阻塞)。

总而言之,你不能依赖 MOC 操作在哪个线程上运行,基本上永远不会。我已经了解到,如果您使用 GCD 或直接使用线程,您总是必须为每个操作创建本地 MOC,然后将它们合并到主 MOC。

有一个很棒的库(MagicalRecord)可以让这个过程变得非常简单。

于 2012-08-06T16:20:33.037 回答