掌握CoreData(第11部分:多线程并发规则)

假设您是在首次启动应用程序时将数百个或数千个主线程上的记录从捆绑数据导入Core Data中的? 结果可能是戏剧性的。 例如,您的应用程序可能由于启动时间太长而被Apple的监视程序杀死,这会显着降低UI性能,甚至可能导致其完全冻结,这不是良好的用户体验。 使用并发,您可以将任务导入其他线程,从而使您的主线程空闲,并且用户可以在不知道后台任何内容的情况下进行交互。

核心数据并发

并发是同时处理多个队列上的数据的能力。 提交到这些队列的工作在线程上执行。

核心数据,多线程和主线程

在核心数据中,作为核心数据堆栈核心的托管对象上下文可以与两种并发模式一起使用,这两种并发模式由NSMainQueueConcurrencyType和NSPrivateQueueConcurrencyType定义。

NSMainQueueConcurrencyType

专用于与您的应用程序界面一起使用,并且只能在始终在主线程上运行的应用程序的主队列中使用 如前所述,它只能在与应用程序界面(UI)相关的工作中使用。 避免对此进行数据处理 。 ,就像将数据从JSON导入Core Data一样

NSPrivateQueueConcurrencyType

配置在初始化时创建自己的队列,并且只能在该队列上使用。 因为该队列是私有的,并且在NSManagedObjectContext实例内部,所以只能通过performBlock: performBlockAndWait :方法对其进行访问。 在进行编码部分时,我们将对此进行深入研究。

零件中使用了 什么托管对象上下文

 让managedObjectContext = appDelegate.persistentContainer.viewContext 

以前,我们非常频繁地使用上述代码,并且我们从persistentContainer实例属性viewContext获取上下文。 顾名思义,其并发类型应为NSMainQueueConcurrencyType。 使用NSPersistentContainer时,viewContext属性被配置为NSMainQueueConcurrencyType上下文

PersistentContainer还具有两种方法 performBackgroundTask:和newBackgroundContext,与之关联的上下文被配置为NSPrivateQueueConcurrencyType。

核心数据旨在在多线程环境中工作。 但是,并非Core Data框架下的每个对象都是线程安全的。 要在多线程环境中使用Core Data,请确保:

  1. 受管对象上下文绑定到初始化时与之关联的线程(队列)
  2. 从上下文中检索到的受管对象被绑定到上下文所绑定到的同一队列

(将通过本节中的代码看到这两点)

规则1

受管对象上下文绑定到初始化时与之关联的线程(队列)。

转到目标→编辑方案→参数→添加参数“ -com.apple.CoreData.ConcurrencyDebug 1 ”,如图2和3所示。我们只是启用了Core Data Concurrency Debugging

如您在图4中看到的,我们正在打破这一规则。 我们正在使用在其他线程的主线程上创建的托管对象上下文。 我们通过执行许多任务来完成此任务

  1. 首先使用持久容器 viewContext属性在主线程/主队列(NSMainQueueConcurrencyType)上创建上下文,该属性在主线程上创建上下文
  2. 使用DispatchQueue方法创建了另一个线程,它将切换该线程
  3. 我们在后台线程上访问了主队列上下文。 那时调试器将停止应用程序,并告诉您正在其他线程上使用上下文。 如果禁用调试配置,则可以使用,但是在某些情况下会导致问题。 正如我之前说的,如果您遵循规则,那么线程化对您来说很容易,否则您将随机崩溃。 因此,要成为好公民,您应遵循“ 在线程1上创建的上下文始终在该线程中访问此上下文 ”的规则。

在图4上,我们在主上下文上创建User ,但是我们正在执行的线程不是主线程

Core Data具有在创建的线程上自动执行上下文的功能,但是您只需要做一些工作(如图5所示),就可以添加一点点配置来实现此目的

  1. 如前所述,在主线程上创建上下文
  2. 像我们之前所做的那样,使用DispatchQueue以某种方式更改线程
  3. 我们使用过的erform(_ 🙂来确保在为上下文指定的队列上执行块操作。 由于上下文是在主线程上创建的erform(_ 🙂 将线程更改为主线程 。 您也可以使用performAndWait(_ 🙂方法
  4. 现在,在perform(_ 🙂块中,所有表达式都将在主线程上执行。 因为它首先检查上下文线程,并且与当前线程无关,所以它将更改线程

erform(_ 🙂performAndWait(_ 🙂确保在为上下文指定的队列上执行块操作。 erform(_ 🙂方法立即返回,并且上下文在其自己的线程上执行block方法。 与performAndWait(_ 🙂 方法,上下文仍将在其自己的线程上执行块方法,但该方法直到块执行后才返回。

经验法则 :如果不确定线程​​。 使用格式(_ 🙂performAndWait(_ 🙂方法。 核心数据会自动从您那里承受这种压力,因为managedObjectcontext也正在跟踪其线程。

摘要规则1

注意: 永远不要在后台线程中使用主队列上下文。 这违反了线程限制规则。 这是一个违反线程的行为,这意味着它将在大多数时间工作,然后在生产中失败,并有很高的数据损坏风险

主队列上下文仅应从主队列(UI队列/线程)或performBlock中进行访问。 如果您需要执行与UI无关的任务,则应创建一个私有队列上下文并通过performBlock访问它。

要确认您的核心数据线程正确无误,可以打开-com.apple.CoreData.ConcurrencyDebug 1运行时设置

规则二

从上下文中检索到的受管对象被绑定到上下文所绑定到的同一队列

如果您要100%地处理核心数据中的多线程,则您将违反此规则;如果您违反此规则,则应用程序将崩溃,而我经常遇到此崩溃。 为了说明这一点,我们首先需要禁用调试以使应用程序真正崩溃,如图6所示。

添加异常断点以捕获异常,如图7所示。

在此处下载启动项目,如果您已经先删除了该应用程序。 我们将研究与任务实体具有很多关系的用户实体,如图8所示。

在图9中,我们打破了规则2,我们在主线程上下文上创建了User对象,而Task对象是在私有上下文(其他线程)上创建的,并且我们将该任务与用户对象相关联,如图10应用程序崩溃并说“ 尝试”在两个不同的上下文之间建立关系 ”。 我们通过执行以下任务来完成此任务

  1. 使用持久性容器viewContext属性在主线程上创建了NSMangedObjectContext
  2. 使用持久性容器newBackgroundContext()方法在后台线程上创建了另一个NSManagedObjectContext ,它将使用并发类型NSPrivateQueueConcurrencyType来提供上下文。
  3. 在主线程上下文中创建用户对象
  4. 在私有线程上下文中创建任务对象
  5. 当我们将任务分配给引发的用户对象异常时,如图9和10所示

继续执行程序,您将获得此崩溃报告“ 不同上下文中的对象之间的关系 ”,如图10所示。

如您所见,用户正在尝试访问在不同上下文中导致问题的对象。

一条经验法则是,对象及其所有关系属性必须在创建对象的同一上下文中创建。 问题是如何访问其他对象,我们将在后面讨论。

在研究并发策略之后,我们将研究解决方案,在父子策略之后,我们将使用NSManagedObjectID解决此问题

摘要

在第11部分中,我们寻求实现并发而没有任何问题的两条规则。

接下来是什么?

在下一部分中,我们将探讨并发问题

有用的链接

https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreData/Concurrency.html

https://medium.com/shakuro/introduction-to-ios-concurrency-a5db1cf18fa6

https://cocoacasts.com/swift-and-cocoa-fundamentals-threads-queues-and-concurrency
https://developer.apple.com/documentation/coredata/using_core_data_in_the_background

https://developer.apple.com/documentation/coredata/nsmanagedobjectcontext

https://medium.com/@marcosantadev/core-data-notifications-with-swift-acc8232a674e

https://marcosantadev.com/core-data-notification-swift/

https://www.youtube.com/watch?v=_QolYhiKWvU