核心数据:父上下文阻塞子

Posted

技术标签:

【中文标题】核心数据:父上下文阻塞子【英文标题】:Core Data: parent context blocks child 【发布时间】:2015-04-27 15:41:54 【问题描述】:

我正在使用核心数据的应用程序中进行一些后台处理。后台处理在子 managedObjectContext 上完成。上下文初始化:

appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate

// the moc in appDelegate is created with .MainQueueConcurrencyType
mainThreadMOC = appDelegate.managedObjectContext!
backgroundMOC = NSManagedObjectContext(concurrencyType:NSManagedObjectContextConcurrencyType.PrivateQueueConcurrencyType)
backgroundMOC?.parentContext = mainThreadMOC

后台处理方式如下:

// download all new transaction log entries
func syncItems() 

... set up the query object for parse

let moc = CoreDataStore.sharedInstance.backgroundMOC

// perform download
moc?.performBlock( 
    self.runQuery(query)   // Download stuff und do some core data work
    )

调试器显示块内的所有工作确实都在后台线程中。

当我从主线程调用此函数并立即使用冗长的核心数据操作阻塞主线程(用于测试目的)时,我看到后台线程停止并且仅在主线程空闲时继续执行。

// this is called from a view controller in the main thread

syncItems() // should start to work in background
for i in 0...200 
    // do some core data work in main thread

// syncItems starts to work after the blocking for-loop ends.

为什么会这样?

【问题讨论】:

子上下文不适用于后台处理 【参考方案1】:

不要使用父子上下文设置。

父子上下文对于任何事情都不是一个好方法。只需使用一个简单的堆栈:两个上下文和一个共享的持久存储协调器。

父子上下文只会增加很多混乱,而不会给您带来任何好处。这是一个非常被误解的概念。我希望像 mzarra 这样的人会停止鼓吹这种设置。这是对社区的伤害。

如果您的背景上下文是主上下文的子上下文,则无论何时需要保存背景上下文,您都必须锁定主上下文和背景上下文。这会阻止 UI。您必须第二次锁定 UI,才能将这些更改从 UI 传播到 PSC。如果您使用背景上下文,则必须将更改合并到主上下文中,但您只会为当前在该上下文中注册的那些对象工作。如果您添加了新对象,或者更新/删除了当前未注册(引用)的对象,则基本上是无操作。

【讨论】:

问题是它被推荐用于帮助多线程,而它被设计为用于对主上下文进行原子更改的暂存器,如果出现错误可以丢弃,这对. 是的。作为便签本,它们可能是一种合理的方法。但是那些推荐它作为多线程处理方式的人根本不知道他们在说什么。 100% 同意,令人震惊的是,一些知名开发人员犯了这个错误!我可能会回顾并观看 WWDC 视频,看看是否有任何线索可以解释为什么会发生这个巨大的错误。顺便说一句,对不起,我在第一条评论中弄错了“是”和“它”。 一个上下文使用MainQueueConcurrencyType,一个使用PrivateQueueConcurrencyType。您可以通过收听 did-save-notifications 并将它们与mergeChangesFromContextDidSaveNotification() 合并来合并更改。这在很多地方都有描述,包括出书:objc.io/books/core-data 一段时间后再次阅读 Daniel 的回答,他 100% 正确地认为人们提倡使用亲子作为背景是对社区的伤害。 API 滥用的完美例子。【参考方案2】:

当你在评论中说“在主线程中做一些核心数据工作”时 工作访问mainThreadMOC

听起来可能主线程工作正在锁定runQuery 需要访问的东西。

尝试将阻塞主线程的测试繁忙工作更改为不访问核心数据的东西(NSThread.sleepForTimeInterval 应该这样做),看看是否让后台runQuery 工作。

如果这是问题所在,您需要将主线程工作重构为不会阻塞 runQuery 中发生的任何事情的东西。

【讨论】:

【参考方案3】:

我怀疑你的问题是双重的,即使你是为了测试目的,你的

冗长的核心数据操作

在主线程上阻塞了 UI,根据您的定义,您的 backgroundMOC?.parentContext = mainThreadMOC

我建议为您的多个NSManagedObjectContexts 创建一个更健壮的结构。

我建议您按照this answer 中列出的步骤进行操作。

此外,您还可以专门创建一个额外的 MOC 来管理您的 runQuery...

discreteMOC = NSManagedObjectContext(concurrencyType:NSManagedObjectContextConcurrencyType.PrivateQueueConcurrencyType)
discreteMOC?.parentContext = backgroundMOC //MOCprivate in my previous answer. 

希望这会有所帮助。

【讨论】:

您如何评论丹尼尔上面的回答?在子(主队列)/父(私有队列)设置中,如果父忙于处理(例如保存),则子只能在尝试从其父存储(父)获取时等待。 @DanielEggert 可能精通该主题,但他的回答主要表明了意见。我没有体验过他提到的 UI 块。我想尝试他的建议,在我的代码中删除父子上下文,但在那之前我无法评论这是否是好的建议。 Core Data 的每个实现都有不同的需求——我的似乎受益于使用父子上下文,但它们是否有必要?我会问他为什么对 Marcus Zarra 的做法如此不满?似乎来自一个诚实的前提。它对我帮助很大,我学到了很多关于 Core Data 的知识。 感谢 rspns:-) 在我的项目中,父背景子主(加上临时便笺簿上下文)设置确实遇到了阻塞问题。在父上下文(成千上万个对象)中保存(删除)时,子级只能在需要获取时等待。这会导致沙滩球滚动几秒钟(真的很烦人)。我通过延迟保存到应用程序变为非活动状态来“伪装”该问题,但这引入了另一个问题:由于 obj 尚未删除,它们仍然出现在 fetch 结果中。所以我不得不做一些丑陋的黑客攻击,但它并没有像预期的那样 100% 工作。 @Lshi 太酷了!老实说,我花了很长时间才了解父子上下文模型(PCCM),但一旦我做到了,它就变得有意义了。我仍然没有声称非常了解线程。对于我的实现,使用 PCCM 时的关键是在主上下文线程(子)上定期执行保存到内存(快速)并在私有上下文线程(父)上保存到磁盘(慢)显然与 PSC 挂钩私人父母。 PCCM 意味着没有混乱的通知或合并编码。私有上下文将管理数据接口并确保与 PSC 的交互不会阻塞 UI。 在该设置中,如果主队列上下文想要同时从父级获取,则私有上下文的活动可能会阻塞 UI……因为父级恰好很忙。目前我正在尝试解决“隐藏那些将被删除但尚未删除的对象”问题。这听起来像是一个糟糕的主意,但我需要尝试不同的方法来缓解阻塞(至少在表面上)。【参考方案4】:

关于父母的情况,不幸的是,我上次检查文档时相当简陋,在网上你会发现几乎每个人都颠倒了流程。早在 2011 年左右,有一次关于 Core Data 的 WWDC 会议,一切都变得清晰,如果你仔细查看 API,它就会开始变得有意义。

孩子不是背景上下文 - 它是主要上下文。孩子是您通常与之交互的上下文。父级是背景上下文。

子级将更改推送给其父级。这就是为什么父级有一个persistentStoreCoordinator,而子级有一个parentContext(它不需要一个persistentStoreCoordinator)。

因此,子项是您的主要上下文,位于主 (UI) 队列中。它将更改保存到其父级中。这发生在内存中(快速 - 让 UI 尽可能响应)。

然后,父上下文通过其 persistentStoreCoordinator 将其更改保存到持久存储中。这就是父母在私人队列中的原因。

lazy var managedObjectContext: NSManagedObjectContext = 
    let parentContext = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
    parentContext.persistentStoreCoordinator = self.persistentStoreCoordinator

    let managedObjectContext = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType)
    managedObjectContext.parentContext = parentContext
    return managedObjectContext
()

还有其他优化,例如将 .undoManager 设置为 nil,但这种通用架构完美适用于后台保存。

您可能还想放入一个接收完成块/闭包的保存方法,立即保存在您的子队列中(如前所述,它只保存到父队列中),然后调用您将拥有的 parentContext 的 performBlock 方法它将(在其私有队列上)保存到底层持久存储中(相对较慢,但现在非阻塞),然后调用您的完成块/关闭(您已经使用 GCD 设置它以在主队列上运行,或者否则,您在父级的 performBlock 方法中从那里回调到主队列。

如果您不反转架构,一切都会完美到位。我不确定它是如何在网上开始的,但这几乎是一个普遍的错误。

祝你好运。

【讨论】:

这不是正确的方法,因为如果孩子在主线程上,当它保存到父线程时,它必须等待后台队列完成保存,因为毕竟用户不能如果他们的操作由于某种原因不成功,请继续,例如他们输入的数据错误或合并错误。这否定了您尝试的任何性能优势,实际上只是包括了不必要的复杂性和内存开销。不要使用父/子线程,它根本不是为它设计的。它被设计为一个便笺簿,用于丢弃编辑对象。 @malhal:我没有完全理解你的观点是为了父母/孩子的主要目的?你能举个例子说明什么是放弃编辑对象? 将其视为“更改的原子性”。例如,想象在第一个视图控制器中创建一个新的 Category 对象,然后在第二个视图控制器上创建一个新的 Product,最后一个保存按钮。如果您刚刚使用了主上下文,并说 Product 条目被取消,或者验证失败,那么您将在上下文中留下一个未保存的类别。如果您创建了一个子上下文并将其传递给视图控制器,那么在保存阶段,如果它失败或被取消,那么该上下文将被丢弃,而主上下文保持一致状态。 @malhal:我非常感谢您的 cmets/解释。但我仍然有一个问题:有没有办法防止上下文阻止对 store/db 文件的访问(比如在保存大量数据时),所以访问同一个 store 的另一个上下文总是可以在不等待的情况下获取?当数据数以千计且是分层的时,此答案中的“这发生在内存中(快速...)”中的陈述不再成立。谢谢。 @LShi 是的,使用背景上下文

以上是关于核心数据:父上下文阻塞子的主要内容,如果未能解决你的问题,请参考以下文章

核心数据:父上下文和变更传播

核心数据父/子上下文保存失败

核心数据:父/子托管对象上下文是不是适合更新一组多个对象?

核心数据:更新子上下文

与核心数据的关系错误

在子上下文中保存核心数据不起作用