【问题标题】:What does Apple mean when they say that a NSManagedObjectContext is owned by the thread or queue that created it?当 Apple 说 NSManagedObjectContext 由创建它的线程或队列拥有时,Apple 是什么意思?
【发布时间】:2011-06-15 14:42:22
【问题描述】:

似乎在 11 月,Apple 更新了 NSManagedObjectContext Class ReferenceCore Data Programming Guide 文档,以明确祝福串行 GCD 调度队列和 NSOperationQueues 作为同步访问 NSManagedObjectContext 的可接受机制。但他们的建议似乎模棱两可,甚至可能自相矛盾,我想确保自己理解正确。

以前公认的观点似乎是 NSManagedObjectContext 只能从创建它的线程访问,并且使用串行队列进行同步是不够的;虽然串行队列一次只执行一项操作,但这些操作可能会被安排在不同的线程上,而 MOC 不喜欢这样。

但是现在,根据编程指南,我们有:

您可以使用线程、串行操作队列或调度队列来实现并发。为简洁起见,本文始终使用“线程”来指代其中的任何一个。

到目前为止,一切都很好(尽管它们将线程和队列混为一谈没有帮助)。所以我可以安全地为每个(串行)队列使用一个上下文,而不是每个操作/块一个,对吧? Apple 甚至在 Core Data WWDC 会议中对此进行了可视化描述。

但是...您在哪里为队列创建上下文?在NSManagedObjectContext 文档中,Apple 声明:

[A context] 假定默认所有者是分配它的线程或队列——这由调用其 init 方法的线程决定。因此,您不应该在一个线程上初始化上下文,然后将其传递给另一个线程。

所以现在我们有了NSManagedObjectContext 需要知道它的所有者是谁的想法。我假设这意味着队列中要执行的第一个操作应该创建 MOC 并保存对它的引用以供其余操作使用。

这是对的吗?我犹豫的唯一原因是NSManagedObjectContext 文章继续说:

相反,您应该将引用传递给持久存储协调器,并让接收线程/队列创建一个从中派生的新上下文。如果使用 NSOperation,则必须在 main(对于串行队列)或 start(对于并发队列)中创建上下文。

Apple 现在似乎将操作与安排其执行的队列混为一谈。这让我很头疼,让我想知道他们是否真的希望你为每个操作创建一个新的 MOC。我错过了什么?

【问题讨论】:

    标签: objective-c multithreading cocoa core-data


    【解决方案1】:

    NSManagedObjectContext 和与之关联的任何托管对象都应固定到单个参与者(线程、序列化队列、最大并发 = 1 的 NSOperationQueue)。

    这种模式称为线程限制或隔离。 (thread || serialized queue || NSOperationQueue with max concurrency = 1) 没有一个很好的短语,所以文档继续说“当我们的意思是,我们将在 Core Data doc 的其余部分使用'thread'获得序列化控制流的这 3 种方法中的任何一种"

    如果您在一个线程上创建一个 MOC,然后在另一个线程上使用它,那么您通过将 MOC 对象引用暴露给两个线程而违反了线程限制。简单的。不要这样做。不要越过溪流。

    我们显式调用 NSOperation,因为与线程和 GCD 不同,它有一个奇怪的问题,即 -init 在创建 NSOperation 的线程上运行,而 -main 在运行 NSOperation 的线程上运行。如果你正确地眯着眼睛看它是有道理的,但它并不直观。如果你在 -[NSOperation init] 中创建你的 MOC,那么 NSOperation 会在你的 -main 方法甚至运行并且你被冲洗之前帮助违反线程限制。

    我们积极劝阻/不赞成以任何其他方式使用 MOC 和线程。虽然理论上可以做 bbum 提到的事情,但没有人做对。每个人都被绊倒了,忘记了一个必要的调用 -lock 在一个地方,“init 在哪里运行?”,或者以其他方式超越了自己。使用自动释放池和应用程序事件循环以及撤消管理器和可可绑定和 KVO,在您尝试将 MOC 传递到其他地方后,一个线程可以通过多种方式保持对 MOC 的引用。在他们开始调试之前,这甚至比高级 Cocoa 开发人员想象的要困难得多。所以这不是一个非常有用的 API。

    更改了文档以阐明和强调线程限制模式是唯一合理的方法。您应该考虑尝试在 NSManagedObjectContext 上使用 -lock 和 -unlock 来更加花哨(a)不可能并且(b)事实上已弃用。它并没有从字面上被弃用,因为代码的工作原理和以往一样好。但是你使用它的代码是错误的。

    有些人在 1 个线程上创建 MOC,然后将它们传递给另一个线程而不调用 -lock。那从来都是不合法的。创建 MOC 的线程一直是 MOC 的默认所有者。对于在主线程上创建的 MOC,这成为一个更常见的问题。主线程 MOC 与应用程序的主事件循环交互,用于撤销、内存管理和其他一些原因。在 10.6 和 iOS 3 上,MOC 更积极地利用由主线程拥有的优势。

    虽然队列没有绑定到特定的线程,但如果您在队列的上下文中创建 MOC,那么正确的事情就会发生。您的义务是遵循公共 API。

    如果队列是序列化的,您可以与在该队列上运行的后续块共享 MOC。

    所以在任何情况下都不要将 NSManagedObjectContext* 暴露给多个线程(actor 等)。有一种歧义。您可以将 didSave 通知中的 NSNotification* 传递给另一个线程的 MOC 的 -mergeChangesFromContextDidSaveNotification: 方法。

    【讨论】:

    • 在处理 didSave 时,我总是接受通知并将其传递给正确的线程,然后再将其应用到 MOC。这是不必要的吗?例如,我在主线程上有一个 MOC,在 NSOperations 中有一个或多个 MOC,并且主线程注册了 didSave 通知。当它收到它时,它会将通知传递给主线程,然后将其交给 MOC。
    • 对 -mergeChangesFromContextDidSaveNotification 的调用必须发生在正确的线程/队列上。 NSNotification 对象本身可以从另一个线程的 MOC 中保留并传递给它。由于 NSNotification 的 userInfo 字典包含来自发布通知的 MOC 对 NSManagedObject 的引用,因此 API 合同的字面阅读可能会得出非法结论。但是 -mergeChangesFromContextDidSaveNotification: 很特别。您的代码无法在另一个线程上浏览 NSNotification 的 userInfo。 mergeChanges的框架代码很用心
    • 对不起,但我必须不同意您的“但是您的代码使用它是错误的”评论。如果 SDK 类不是线程安全的/无法处理来自多个线程的访问,那么唯一“错误”的是 SDK 类的实现。不要通过暗示突出实施中缺陷的代码本身是不正确的来为有缺陷的实施辩护。
    • 实际上,@aroth,使用这种风格的代码几乎不可能是正确的。此外,这不是“线程安全”或“非线程安全”(或“假定但不是线程安全”)的问题。 Core Data 一直都有一个定义良好的线程模型——只是很多人天真地假设他们可以在多个线程上做事,或者没有框架代码访问他们创建的上下文。两者都错了。
    • @ChrisHanson 我同意你的观点“......你的代码是错误的”(因为:这是 SDK 的编写方式;作为开发人员,我们的代码必须适合 API,而不是其他方式)。但是,公平地说:SDK 首先是“错误的”——在访问 MT 时,任何 API 都不应该静默失败(至少:它应该抛出异常,CoreData 不这样做,而是它损坏您的数据库!)。其他仅是单线程的 Apple API,并且有充分的理由,历来被标记为错误并得到修复(例如在 CoreAnimation 和 UIKit 渲染中)。
    【解决方案2】:

    听起来你说得对。如果您使用线程,则需要上下文的线程需要创建它。如果您使用队列,则需要上下文的队列应该创建它,最有可能作为在队列上执行的第一个块。听起来唯一令人困惑的部分是关于 NSOperations 的部分。我认为 NSOperations 的混乱并不能保证它们运行在哪个底层线程/队列上,因此即使它们都在同一个 NSOperationQueue 上运行,在操作之间共享 MOC 也可能不安全。另一种解释是它只是令人困惑的文档。

    总结一下:

    • 如果您使用线程,请在需要它的线程上创建 MOC
    • 如果您使用 GCD,请在串行队列上执行的第一个块中创建 MOC
    • 如果您正在使用 NSOperation,请在 NSOperation 内部创建 MOC,并且不要在操作之间共享它。这可能有点偏执,但 NSOperation 不保证它运行在哪个底层线程/队列上。

    编辑:根据 bbum 的说法,唯一真正的要求是访问需要被序列化。这意味着您可以跨 NSOperations 共享一个 MOC,只要这些操作都添加到同一个队列中,并且该队列不允许并发操作。

    【讨论】:

    • 提交文档错误。基本含义相当简单; MOC 可以跨线程/队列/等共享只要一次只有一个线程/队列/等使用它。 IE。对 MOC 的所有访问都应在执行方面进行序列化。
    • @bbum 很高兴听到。我怀疑它就这么简单,但是文档谈到拥有队列的线程/队列而不是仅仅说需要序列化访问这一事实让我有点偏执。
    • @Daniel 有趣——我会请教专家。
    • 其实这种情况下bbum是不正确的;你真的应该在将要使用它的线程上创建上下文。例如,如果您在主线程上创建一个,它将期望在主运行循环结束时处理更改。请参阅 -[NSManagedObjectContext processPendingChanges]。或者更好的是,只需阅读下面 Ben 的回复......
    • 我似乎从经验中学到的一个痛苦的教训是,在 iOS 4(但不是 5)下,如果您使用串行调度队列,并且您的初始上下文创建由主服务器的 dispatch_sync 执行线程然后上下文可以最终从该线程创建(尽管不是在主队列上),稍后会导致通常的随机线程限制相关问题(即,随机无意义的 SIGSEGV,关于不存在的抽象属性的警告,等等)。切换到dispatch_async 在实践中为我解决了这个问题,但并没有让我充满信心。有人可以评论吗?
    猜你喜欢
    • 2013-06-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-07-01
    • 1970-01-01
    • 2012-01-21
    • 1970-01-01
    相关资源
    最近更新 更多