【问题标题】:NSManagedObjectContext performBlockAndWait: doesn't execute on background thread?NSManagedObjectContext performBlockAndWait:不在后台线程上执行?
【发布时间】:2012-08-03 15:22:12
【问题描述】:

我有一个这样声明的 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会在主线程而不是后台线程上执行任务?

【问题讨论】:

    标签: iphone objective-c ios core-data


    【解决方案1】:

    抛出另一个答案,尝试解释为什么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 拥有队列,而不是相反 大约。如果我错了,请纠正我。 – Philip007

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

    现在,在我深入细节之前,我想强调一下,依赖库的内部运作是非常危险的。您真正应该关心的是,您永远不能期望特定线程执行块,除非与主线程相关联。因此,不建议期望 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 都必须做出一些“糟糕”的决定来支持它......比如拥有同步版本“跳”队列。

    【讨论】:

    • 您确定“performBlockAndWait 不使用内部队列”吗?我认为确实如此。唯一的区别是performBlockAndWait 将等到块完成。 calling thread 是什么意思?据我了解,[moc performBlockAndWait][moc performBloc] 都在其私有队列(后台或主)上运行。这里的重要概念是 moc 拥有队列,而不是相反。如果我错了,请纠正我。
    • 假设我的代码只是在后台线程上运行。我得到了mainContext 并使用performBlockAdnWait: 执行一些操作@。即使使用了属于主线程的主上下文?
    • 相对于作为接收器的 MOC performBlock* 方法。现在,请注意 performBlockAndWait 应该很少被使用,并且只有当你有一个非常好的理由(即,否则它不起作用) - 从经验来看,这是非常罕见的。
    • @JodyHagins performBlockAndWait(和performBlock)将始终在专用指定队列中执行。这很重要,因为这保证了并发约束。在示例中,performBlockAndWait 恰好在主线程上执行,因为它是调用站点提交将被同步调用的块的主线程。这只是一个优化,dispatch lib 将在调用站点的线程(已记录)上执行块,这可能在块同步时发生> 发送。请注意,GCD 可以选择任何线程来执行块。
    • @JodyHagins 明确一点:提交到某个调度队列的块将总是执行on该队列。当一个块将被执行时,GCD分配一个线程给这个队列。该线程可能会针对某个队列而更改 - 但不会针对单个块。它可以是分配给队列的任何线程。只有一个例外:main_queue 总是使用主线程来执行块。
    【解决方案2】:

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

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

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

    【讨论】:

    • Core Data 在内部不使用 dispatch_sync 来进行 performBlockAndWait。您会从文档中注意到 performBlockAndWait 是可重入的。
    • 我没这么说。我说它使用相同的想法。
    【解决方案3】:

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

    【讨论】:

    • 但是如果我在块中有与该上下文的后台线程相关的代码怎么办?它在主线程上运行不是很危险吗?例如,如果我执行 [backgroundMOC save:nil];,并且在主线程而不是 bg 线程上执行,这不是问题吗?
    • 不,因为 MOC 向您保证,如果您在 performBlock:performBlockAndWait: 块内对 backgroundMOC 执行任何操作,那么您可以避免线程问题。 (换句话说,它可能会阻止任何代码在您的块运行时在后台队列上运行。)
    • 所以如果我使用 performBlock:,我不应该对块内的线程做任何断言语句吗?还是仅仅为了 performBlockAndWait:?
    • performBlockAndWait,顾名思义,等待块完成执行,因此不会发生任何不好的事情。我认为块在同一个线程上执行是一种性能优化。 - 是的,你不应该假设块是在哪个线程上执行的。
    • "那么 performBlock: for the backgroundMOC 有可能在主线程上运行吗?"是的,因为不能保证它不会。您可以通过使用主队列上下文来保证块将在主队列上运行,但您不能通过使用私有队列上下文来保证它不会。此外,如果上下文的父级是主队列上下文,则工作可能会因为此块而被排入队列。
    【解决方案4】:

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

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2013-04-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多