多线程操作Coredata(转)

第一步:搭建 Core Data 多线程环境
这个问题首先要解决的是搭建 Core Data 多线程环境。Core Data 对并发模式的支持非常完备,NSManagedObjectContext 的指定初始化方法中就指定了并发模式:

init(concurrencyType ct: NSManagedObjectContextConcurrencyType)

有三种模式:
1.NSConfinementConcurrencyType
这种模式是用于向后兼容的,使用这种模式时你应该保证只能在创建的线程里使用 context,然而这不容易得到保证。关于此模式的最新消息是 iOS 9 中它将被废弃,不推荐使用。
2.NSPrivateQueueConcurrencyType
在一个私有队列中创建并管理 context。
3.NSMainQueueConcurrencyType
其实这种模式与第2种模式比较相似,只不过 context 与主队列绑定,同时也因此与应用的 event loop 紧密相连。当 context 与 UI 更新相关的话就使用这种模式。

从 iOS 9 开始就剩下后面两种模式了,那么搭建多线程 Core Data 环境的方案一般如下,创建一个 NSMainQueueConcurrencyType 的 context 用于响应 UI 事件,其他涉及大量数据操作可能会阻塞 UI 的,就使用 NSPrivateQueueConcurrencyType 的 context。

let mainContext = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType)
let backgroundContext = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)

但需要注意的是「Core Data 使用线程或者序列化的队列来保护 managed objects 和 managed object context,因此 context 假设它的默认拥有者是它初始化时分配的线程或队列,你不能在某个线程中初始化一个 context 后传递给另外一个线程来使用它。」这段蹩脚的话是我从NSManagedObjectContext的文档翻译来的,意思就是说 managed object context 并非线程安全的,你不能随便地开启一个后台线程访问 managed object context 进行数据操作就管这叫支持多线程了,那么应该怎么做呢?官方文档《Using a Private Queue to Support Concurrency》为我们做了示范,在 private queue 的 context 中进行操作时,应该使用以下方法:

 func performBlock(_ block: () -> Void)//在私有队列中异步地执行 Blcok
 func performBlockAndWait(_ block: () -> Void)//在私有队列中执行 Block 直至操作结束才返回

要在不同线程中使用 managed object context 时,不需要我们创建后台线程然后访问 managed object context 进行操作,而是交给 context 自身绑定的私有队列去处理,我们只需要在上述两个方法的 Block 中执行操作即可。事实上,你也可以在其他线程中来使用 context,但是要保证以上两个方法。而且,在 NSMainQueueConcurrencyType 的 context 中也应该使用这种方法执行操作,这样可以确保 context 本身在主线程中进行操作。

题外话,在构建多线程 context 时,经常会出现这样的局面:Multi-contexts vs Concurrency。前者可能有更加复杂的情况,在 iOS 5之后,context 可以指定父 context,persistent store coordinator 不再是其与 persistent store 联系的唯一选择。需要注意的是,子 context 的 fetch 和 save 操作都会交给父 context 来完成,对于子 context 的 save 操作,只会到达上一层的父 context 里,只有父 context 执行了 save 操作,子 context 中的变化才会提交到 persistent store 保存。这种子 context 适合在后台执行长时间操作,比如在后台里在子 context 里导入大量数据,在主线程的父 context 里更新进度。另外一种是平行的多 context。Concurrency 的特性也是在 iOS 5 后开始支持,这个特性减少了平行的多 context 的需求。关于这个话题,可以看这篇文章:《Concurrent Core Data Stacks – Performance Shootout》

第二步:数据的同步操作
总的来说,在多 context 环境的下,context 的生命周期里有两个阶段需要处理数据同步的问题。当某个 context 里的状态发生了变化并执行保存来更新 persistent store后,对于其他 context 来说有两个选择:1. persistent store 更新后,此时其他 context 与 persistent store 进行同步;2. persistent store 更新后,其他 context 并不立即同步,而在自身进行保存时与 persistent store 进行同步,两者有差异时需要解决冲突问题。前者采取的是「一处修改处处同步」的策略,所有的 context 中同步为一个版本,后者采取的是「多版本共存协商处理」的策略。以下讨论都基于多个 context,假设应用配置了两个 managed object context,mainContext,在主线程运行 ,另外一个 backgroundContext 用于后台处理。

多 context 单数据版本

在 context 中执行保存时,应用并不会主动告知其他 context。那么如何在多个 context 间进行通信呢?Core Data 提供了通知机制,context 执行保存时,会发出以下通知:
1.NSManagedObjectContextWillSaveNotification
Managed object context 即将执行保存操作时发出此通知,没有附带任何信息。
2.NSManagedObjectContextDidSaveNotification
Managed object context 的保存操作完成之后由该 context 自动发出此通知,包含了所有的新增、更新和删除的对象的信息。注意在通知里的 managed objects 只能在 context 所在的线程里使用。由于 context 也只能在自身的线程里执行操作,所以没法直接使用通知里的 managed objects,这时候应该通过 managed object 的 objectID 以及 context 的objectWithID(_ objectID: ) 来获取。

合并其他 context 的数据可以通过mergeChangesFromContextDidSaveNotification(_ notification:)来完成。在这个方法里,context 会更新发出通知的 context 里变化的任何同样的对象,引入新增的对象并处于 faults 状态,并删除在发出通知的 context 里已经删除的对象。

代码示例,在 backgroundContext 中编辑后使 mainContext 与之同步:

NSNotificationCenter.defaultCenter().addObserver(self,
                                             selector: "backgroundContextDidSave:",
                                                 name: NSManagedObjectContextDidSaveNotification,
                                               object: backgroundContext)

func backgroundContextDidSave(notification: NSNotification){
    mainContext.performBlock(){
        mainContext.mergeChangesFromContextDidSaveNotification(notification)
    }
}
多 context 多数据版本

在这种方案下,backgroundContext 并不针对 mainContext 的保存做出反应。在 mainContext 和 backgroundContext 中 fetch 了同类的 managed objects,两个 context 都发生了变化并且变化不一样,此时让 backgroundContext 与 mainContext 前后脚分别执行保存的话,就会发生冲突导致后者保存失败。

在 managed object context 中执行 fetch 操作时,会对 persistent store 里的状态进行快照,当 context 执行保存时,会使用快照与 persistent store 进行对比,如果状态不一致,说明 persistent store 在其他地方被更改了,而这个变化并不是当前 context 造成的,这样就造成了当前 context 状态的不连续,此时保存就会产生冲突。这里需要介绍 managed object context 的属性mergePolicy,这个属性指定了 context 的合并策略,决定了保存时合并数据发生冲突时如何应对,该属性有以下几种值:
1.NSErrorMergePolicy
默认策略,有冲突时保存失败,persistent store 和 context 都维持原样,并返回错误信息,是唯一反馈错误信息的合并策略。
2.NSMergeByPropertyStoreTrumpMergePolicy
当 persistent store 和 context 里的版本有冲突,persistent store 里的版本有优先权, context 里使用 persistent store 里的版本替换。
3.NSMergeByPropertyObjectTrumpMergePolicy
与上面相反,context 里的版本有优先权,persistent store 里使用 context 里的版本替换。
4.NSOverwriteMergePolicy
用 context 里的版本强制覆盖 persistent store 里的版本。
5.NSRollbackMergePolicy
放弃 context 中的所有变化并使用 persistent store 中的版本进行替换。

除了默认的 NSErrorMergePolicy在发生冲突时返回错误等待下一步处理外,其他的合并策略直接根据自身的规则来处理合并冲突,因此在选择时要谨慎处理。从上面的解释来看,似乎NSMergeByPropertyStoreTrumpMergePolicyNSRollbackMergePolicy没什么区别,NSMergeByPropertyObjectTrumpMergePolicyNSOverwriteMergePolicy也没有什么区别。区别在于怎么对待被覆盖的一方中没有冲突的变化(解释见此处)NSMergeByPropertyStoreTrumpMergePolicyNSMergeByPropertyObjectTrumpMergePolicy采取的是局部替换,前者 context 中没有冲突的变化不会受到影响,后者 persistent store 中没有冲突的变化不受影响;NSOverwriteMergePolicyNSRollbackMergePolicy 采取的是全局替换,persistent store 和 context 中只有一方的状态得以保留。

回到本节开始的场景,mainContext 和 backgroundContext 中的版本不一致,会产生合并冲突,解决方案有以下两种选择:
1.不管 mainContext 中是否发生改变,与 backgroundContext 中状态同步;

//此时 mainContext 和 backgroundContext 都采用默认合并策略即可。
mainContext.performBlock(){
    do{
       try mainContext.save()
    } catch {
        /*
        清空 mainContext,其中所有的 managed objects 消失。
        如果引用了其中的 managed objects 的话,注意在 reset 前取消对这些对象的引用。
        */
        mainContext.reset()
        //重新 fetch
        let fetchRequest = ...
        let updatedFetchedResults = try mainContext.executeFetchRequest(fetchRequest)
    }
}

又或者,mainContext 的合并策略采用NSMergeByPropertyStoreTrumpMergePolicyNSRollbackMergePolicy,这样就省去了 reset 操作。实际上,采用这种方案不如上一个策略来得方便。
2.不管其他 context 中发生什么变化,当前 context 进行保存时直接覆盖 persistent store 里的版本,这种方案下 context 的合并策略需要采用NSOverwriteMergePolicyTypeNSMergeByPropertyObjectTrumpMergePolicy,而且执行保存时不会返回错误,不需要后续的处理。

小结

同步多个 context 是个比较复杂的事情,需要根据具体的需要来设定 context 的合并策略以及选择同步的时机,不仅仅限于以上的两种策略,融合两种策略也可以,当然那样可能会大大增加复杂度,更容易导致 Bug。另外,还有一种使用 child context 的方法,就是将其他 context 作为 context 的 parentContext,这种方法没有研究,自己有兴趣可以试试。
1.同步问题第一原则:不要跨线程使用 managed object,而应该通过其对应的 objectID,在其他线程里的 context 里来获取对象。
2.NSManagedObjectContext的合并方法mergeChangesFromContextDidSaveNotification(_ notification:)可以替完全复制另一个 context 的状态;如果你不想完全复制,可以使用更精确的方法refreshAllObjects(),这是 iOS 9 中推出的新方法;或者手动处理,当然,不推荐这么做。
3.利用NSMergePolicy来处理同步相对而言危险一点,你得明确知道你在做什么。

最后一站:大量数据操作

从上面的内容可以得知,在多线程环境下同步数据基本上不需要我们手动去处理 managed objects 的同步,因此处理大量数据的同步,关注的重点更多在于内存占用和性能。写代码要记住以下几点:
1.涉及大量数据的操作尽量要放在后台线程里处理,防止阻塞主线程;对于多 context 的结构,可以参考这篇文章《Concurrent Core Data Stacks – Performance Shootout》,作者通过验证,证明了「设置一个 persistent store coordinator 和两个独立的 contexts 被证明了是在后台处理 Core Data 的好方法」。
2.能够保持 faults 状态的 managed objects 尽量不要触发 fire,降低内存占用,同时也能提升响应速度。
3.fetch 大量数据时注意技巧,可以通过利用 predicate 来筛选实际需要的数据,限制 fetch 的总数量,设定合适的批量获取数量来降低 IO 的频次,这些需要在实际环境中寻找平衡点。
4.尽量让 context 中的 entity 类别少一些,降低对同步的需求。
(从 iOS 8 开始,Core Data 在性能方面有了较大的提升,尽量合理利用。)
5.使用异步请求 Asynchronous Fetch,尽管可以将 fetch 大量数据的操作放在后台线程里,但是这样依然会阻塞那个线程,使用异步请求,则依然可以在后台线程里进行其他操作,并且还有方便的进度提示和取消功能。
6.使用批量更新 Batch Update,有效降低内存占用并大幅提升保存的速度。以往在NSManagedObjectContext中进行保存时,只能将其中包含的变化进行保存,而 Batch Update 则是直接对 persistent store 进行更新而不需要将 managed objects 读入内存,可以大幅降低内存占用而且更新速度提升不少。但需要注意的是,使用批量更新并不会提醒 context,需要我们对 context 手动进行更新,而且没有进行有效验证,也需要开发者来保证有效性。
7.使用批量删除 Batch Delete,与批量更新类似,直接对 persistent store 进行操作,效率非常高,也有着和批量更新类似的问题。
8.使用 iOS 9 新增的NSManagedObjectContext的新 API:refreshAllObjects(),该方法会对 context 中注册的所有对象进行刷新,还没有保存的变化也会得到保留,这样就可以解放 6 和 7 中的手动更新工作。

文/seedante(简书作者)
原文链接:http://www.jianshu.com/p/37ab8f336f76
著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。

时间: 2024-08-06 12:07:43

多线程操作Coredata(转)的相关文章

iOS 数据库多线程操作

SQLite是支持多线程操作的, 需要在初始化database是做一个线程安全的config,参考下面链接,看起来比较复杂. http://www.cnblogs.com/wfwenchao/p/3964213.html 很多iOS项目中都使用FMDB这个第三方开源库,但FMDB不能在多个线程中共同一个FMDatabase对象,因为这个类本身不是线程安全的,如果这样使用会造成数据混乱等问题. 如要需要多线程操作数据库,那么就需要使用FMDatabaseQueue来保证线程安全了. 首先用一个数据

黑马程序员-Java多线程操作

--Java培训.Android培训.iOS培训..Net培训.期待与您交流!--- Java中的线程 一个程序的运行需要启动一个应用进程,一个进程可以创建多个线程,帮助应用完成多任务操作,实现并发运行.在Java中线程是被封装成Thread类,进行多线程操作时只需要继承一个Thread类,实现自己的功能即可,然后开启此线程,或者你也可以实现一个Runnable接口,然后将其传递给Thread对象,然后再启动它. 线程的创建于启动 继承Thread 创建一个类并继承Thread类,然后实现Thr

IE exp中的多线程操作

最近在写cve-2014-0321这个漏洞的poc,分析的时候发现这流程简直就是个坑(越写越坑!!),没办法只能照着古河的思路,利用多线程进行操作.虽然知道多线程操作用window.open去创建,但调用了之后发现一直创建的都是个进程,根本就没有线程啊,简直就是个坑啊.后来无数次的尝试之后才发现,之前一直本地进行IE漏洞的调试,这样子使用window.open打开的总是一个进程,这样子也就没办法实现利用或者是占位.之后,用python架了简易的web服务,web的方式来访问,才发现window.

C#中级-常用多线程操作(持续更新)

一.前言       多线程操作一直是编程的常用操作,掌握好基本的操作可以让程序运行的更加有效.本文不求大而全,只是将我自己工作中常常用到的多线程操作做个分类和总结.平时记性不好的时候还能看看.本文参考了多篇园子里的精彩博文,在文章最后会贴出具体来源,感谢他们的无私奉献. 二.关于线程 (1) 为何使用线程: 可以使用线程将代码同其他代码隔离,提高应用程序的可靠性: 可以使用线程来简化编码: 可以使用线程来实现并发执行. (2) 进程.应用程序域以及线程的关系: 进程(Process)是Wind

python threading模块使用 以及python多线程操作的实践(使用Queue队列模块)

今天花了近乎一天的时间研究python关于多线程的问题,查看了大量源码 自己也实践了一个生产消费者模型,所以把一天的收获总结一下. 由于GIL(Global Interpreter Lock)锁的关系,纯的python代码处理一般逻辑的确无法活动性能上的极大提升,但是在处理需要等待外部资源返回或多用户的应用程序中,多线程仍然可以作为一个比较好的工具来进行使用. python提供了两个模块thread和threading 来支持python的多线程操作.通俗的讲一般现在我们只使用threading

DataTable多线程操作报错情况

最近在写一个http接口时用了DataTable这个强大的利器,接口用浏览器跑起来没任何问题.当时也没考虑并发问题,后来用一个压力测试工具做大并发测试,1000+/s次速度测试.发现程序报错了.程序报错了第一反应还是去检查代码,是不是代码出现问题.发现逻辑都是对的,然后用浏览器打开接口,发现一切OK;然后心想肯定是并发时多个线程操作导致的. 我们都知道在多线程的时候不同的线程访问同一个资源的时候,用lock方法来达到线程同步,也就是同一个时刻同一个资源只能被一个线程操作. 我开始在操作DataT

MFC的多线程操作

记得用MFC做了一个图像自动修复软件,当时没有多线程操作这一概念,由于图像修复算法比较复杂,因此,当执行图像修复时,程序就像卡死了似得而不能做其他操作.其实MFC对这种情况有一种很好地解决方案,那就是采用多线程编程技术.以图像修复算法为例,由于其本身需要耗费大量时间,因此我们可以单独开一个线程让他执行修复而不影响主程序的操作. 关于线程的一些概念,以及在VS2013下的实例: MFC中有两类线程,分别称之为工作者线程和用户界面线程.二者的主要区别在于工作者线程没有消息循环,而用户界面线程有自己的

Java多线程操作

Java中的线程 一个程序的运行须要启动一个应用进程,一个进程能够创建多个线程.帮助应用完毕多任务操作.实现并发运行.在Java中线程是被封装成Thread类.进行多线程操作时仅仅须要继承一个Thread类,实现自己的功能就可以.然后开启此线程,或者你也能够实现一个Runnable接口.然后将其传递给Thread对象.然后再启动它. 线程的创建于启动 继承Thread 创建一个类并继承Thread类,然后实现Thread类的run方法,在run方法内填上自己的代码.然后创建这个自己定义类,并调用

C# 多线程操作之异步委托

标签: 多线程任务nullstringhtml工作 2012-06-29 23:00 1276人阅读 评论(0) 收藏 举报  分类: C/C++/C#/dotnet(126)  目录(?)[+] 在应届生找工作的时候,多线程操作几乎是所有的公司都会问及的一个基本问题. 这里做了一个多线程操作的总结,这里总结了通过异步委托来实现多线程操作. 定义一个委托,是创建一个线程的最简单的方法,并且异步调用它.委托是方法的类型安全的引用.同时委托还智齿异步调用方法. 委托使用线程池来完成异步任务. 当自己