【问题标题】:Behavior differences between performBlock: and performBlockAndWait:?performBlock: 和 performBlockAndWait: 之间的行为区别?
【发布时间】:2015-11-18 20:14:26
【问题描述】:

我正在私有队列中创建一个NSManagedObjectContext 来处理我从文件和/或服务中获取的数据更新:

NSManagedObjectContext *privateContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
AppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
privateContext.persistentStoreCoordinator = appDelegate.persistentStoreCoordinator;

由于我使用的是私有队列,所以我不完全理解 performBlock:performBlockAndWait: 方法之间的区别...为了执行我的数据更新,我目前正在这样做:

[privateContext performBlock: ^{

        // Parse files and/or call services and parse
        // their responses

        // Save context
        [privateContext save:nil];

        dispatch_async(dispatch_get_main_queue(), ^{
            // Notify update to user
        });
    }];

在这种情况下,我的数据更新是同步和顺序的,所以我想这是保存上下文的正确位置,对吧?如果我做错了什么,如果你让我知道,我将不胜感激。另一方面,这段代码是等价的吗?:

[privateContext performBlockAndWait: ^{

        // Parse files and/or call services and parse
        // their responses

        // Save context
        [privateContext save:nil];
    }];

// Notify update to user

我再次猜想这是保存上下文的正确位置......这两种方法之间有什么区别(如果有的话,在这种情况下)?

如果我需要执行异步服务调用而不是执行同步服务调用或文件解析,该怎么办?如何管理这些数据更新?

提前致谢

【问题讨论】:

    标签: ios core-data asynchronous synchronization nsmanagedobjectcontext


    【解决方案1】:

    您是正确的,您想对 MOC 执行的任何操作都必须在 performBlockperformBlockAndWait 内完成。请注意,对于托管对象,保留/释放是线程安全的,因此您不必在其中一个块内即可保留/释放托管对象的引用计数。

    它们都使用同步队列来处理消息,这意味着一次只会执行一个块。嗯,这几乎是真的。见performBlockAndWait的描述。在任何情况下,对 MOC 的访问都会被序列化,这样一次只有一个线程在访问 MOC。

    tl;dr 不要担心差异,始终使用performBlock

    事实差异

    有许多不同之处。我敢肯定还有更多,但这里是我认为最重要的理解。

    同步与异步

    performBlock 是异步的,因为它立即返回,并且该块在未来某个时间在某个未公开的线程上执行。通过performBlock 提供给 MOC 的所有块都将按照它们添加的顺序执行。

    performBlockAndWait 是同步的,因为调用线程将等到块执行完毕后再返回。该块是在其他线程中运行,还是在调用线程中运行并不是那么重要,并且是一个无法信任的实现细节。

    但是请注意,它可以实现为“嘿,其他线程,运行这个块。我会坐在这里什么都不做,直到你告诉我它完成了。”或者,它可以实现为“嘿,Core Data,给我一个锁,阻止所有其他块运行,这样我就可以在我自己的线程上运行这个块。”或者它可以以其他方式实现。再次,实施细节,可能随时更改。

    我会告诉你这一点,上次我测试它时,performBlockAndWait 在调用线程上执行了块(意思是上一段中的第二个选项)。这只是帮助您了解正在发生的事情的真正信息,不应以任何方式依赖。

    重入

    performBlock总是异步的,因此不可重入。好吧,有些人可能会认为它是可重入的,因为您可以从使用performBlock 调用的块中调用它。但是,如果您这样做,对 performBlock 的所有调用将立即返回,并且该块将不会执行,直到至少 当前 正在执行的块完全完成其工作。

    [moc performBlock:^{
        doSomething();
        [moc performBlock:^{
          doSomethingElse();
        }];
        doSomeMore();
    }];
    

    这些函数将始终按此顺序执行:

    doSomething()
    doSomeMore()
    doSomethingElse()
    

    performBlockAndWait始终同步。此外,它也是可重入的。多次调用不会死锁。因此,如果您在一个因另一个performBlockAndWait 而正在运行的块内时最终调用performBlockAndWait,那么没关系。您将获得预期的行为,因为第二次调用(以及任何后续调用)不会导致死锁。此外,如您所料,第二个将在返回之前完全执行。

    [moc performBlockAndWait:^{
        doSomething();
        [moc performBlockAndWait:^{
          doSomethingElse();
        }];
        doSomeMore();
    }];
    

    这些函数将始终按此顺序执行:

    doSomething()
    doSomethingElse()
    doSomeMore()
    

    先进先出

    FIFO代表“先进先出”,这意味着块将按照它们被放入内部队列的顺序执行。

    performBlock 始终遵循内部队列的 FIFO 结构。每个块都会被插入到队列中,只有在被移除时才会运行,按照先进先出的顺序。

    根据定义,performBlockAndWait 会破坏 FIFO 排序,因为它会跳过已入队的块队列。

    使用performBlockAndWait 提交的块不必等待队列中正在运行的其他块。有很多方法可以看到这一点。一个简单的就是这个。

    [moc performBlock:^{
        doSomething();
        [moc performBlock:^{
          doSomethingElse();
        }];
        doSomeMore();
        [moc performBlockAndWait:^{
          doSomethingAfterDoSomethingElse();
        }];
        doTheLastThing();
    }];
    

    这些函数将始终按此顺序执行:

    doSomething()
    doSomeMore()
    doSomethingAfterDoSomethingElse()
    doTheLastThing()
    doSomethingElse()
    

    在这个例子中很明显,这就是我使用它的原因。但是,请考虑一下,如果您的 MOC 正在从多个地方调用它。可能有点混乱。

    但要记住的一点是,performBlockAndWait 是抢占式的,可以跳过 FIFO 队列。

    死锁

    调用performBlock 永远不会出现死锁。如果你在块内做了一些愚蠢的事情,那么你可能会死锁,但调用performBlock 永远不会死锁。你可以从任何地方调用它,它会简单地将块添加到队列中,并在将来的某个时间执行它。

    调用performBlockAndWait 很容易导致死锁,尤其是当您从外部实体可以调用的方法或在嵌套上下文中不加选择地调用它时。具体来说,如果父级对子级调用 performBlockAndWait,几乎可以保证您的应用程序会死锁。

    用户事件

    Core Data 认为“用户事件”是调用processPendingChanges 之间的任何事件。您可以阅读有关此方法为何重要的详细信息,但“用户事件”中发生的事情会对通知、撤消管理、删除传播、更改合并等产生影响。

    performBlock 封装了一个“用户事件”,这意味着代码块在对processPendingChanges 的不同调用之间自动执行。

    performBlockAndWait 不封装“用户事件”。如果您希望将块视为不同的用户事件,则必须自己进行。

    自动释放池

    performBlock 将块包装在自己的自动释放池中。

    performBlockAdWait 不提供唯一的自动释放池。如果您需要,您必须自己提供。

    个人意见

    就个人而言,我认为使用performBlockAndWait 的理由并不多。我确信有人有一个无法以任何其他方式完成的用例,但我还没有看到它。如果有人知道这个用例,请与我分享。

    最接近的是在父上下文上调用performBlockAndWait(永远不要在NSMainConcurrencyType MOC 上这样做,因为它可能会锁定您的用户界面)。例如,如果您想确保数据库在当前块返回之前已完全保存到磁盘,并且其他块有机会运行。

    因此,不久前,我决定将 Core Data 视为一个完全异步的 API。结果,我有一大堆核心数据代码,在测试之外我没有一次调用performBlockAndWait

    这样生活会好很多。当我认为“它一定很有用,否则他们不会提供它”时,我遇到的问题比以前少得多。

    现在,我不再需要performBlockAndWait。结果,也许它改变了一些,我只是错过了它,因为它不再让我感兴趣……但我对此表示怀疑。

    【讨论】:

    • 很好的答案。使用performBlock:,如果您需要使用托管对象的属性更新 UI,您通常只是在块末尾分派到 main 吗?`(使用 dispatch_async-[NSOperationQueue mainQueue]
    • @JasonMoore - 我 dispatch_async 到主线程。我喜欢使用一个更易于使用的函数,它是我编写的一个简单的 dispatch_async 包装器,所以我不必指定队列……它更容易阅读。
    • @JodyHagins 不使用performBlockAndWait,你怎么知道核心数据何时完成保存?那么通知是唯一的解决方案吗?
    • @TonyLin 你把你的代码放在performBlock - 在那个块里你保存你的东西,然后你想做什么 - 基本上你把动作链接在一起。
    • 嗨乔迪。我读了你的答案,但有一个不同的问题。你能看到How do you maintain object integrity when you have nested (child/parent) contexts?
    猜你喜欢
    • 1970-01-01
    • 2018-06-02
    • 2015-09-10
    • 1970-01-01
    • 2012-04-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-01-07
    相关资源
    最近更新 更多