当保存在后台异步完成时,我应该如何保证从嵌套上下文中的不同线程获取结果是最新的?

Posted

技术标签:

【中文标题】当保存在后台异步完成时,我应该如何保证从嵌套上下文中的不同线程获取结果是最新的?【英文标题】:How should I guarantee fetch results from a different thread in a nested contexts are up to date, when saves are done asynchronously in background? 【发布时间】:2018-12-31 20:32:17 【问题描述】:

我已阅读以下内容Behavior differences between performBlock: and performBlockAndWait:? 但无法找到我的问题的答案。

以下代码取自 RayWenderlich video。特别是在10:05,代码是这样的something

class CoreDataStack 
    var coordinator : NSPersistentStoreCoordinator

    init(coordinator: NSPersistentStoreCoordinator)
        self.coordinator = coordinator
    
    // private, parent, in background used for saving
    private lazy var savingContext : NSManagedObjectContext = 
        let moc = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
        moc.persistentStoreCoordinator = coordinator
        return moc
    ()

    lazy var mainManagedObjectedContext : NSManagedObjectContext = 
        let moc = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
        moc.parent = self.savingContext
        return moc
    ()

    func saveMainContext() 
        guard savingContext.hasChanges || mainManagedObjectedContext.hasChanges else 
            return
        

        mainManagedObjectedContext.performAndWait 
            do 
                try mainManagedObjectedContext.save()
            catch let error
                fatalError(error.localizedDescription)
            
        

        savingContext.perform 
            do 
                try self.savingContext.save()
            catch let error
                fatalError(error.localizedDescription)
            
        
    

据我了解,主上下文只是将更改传递给它的父上下文,这是一个私有的背景上下文。它同步执行此操作。

然后,父级私有上下文在后台线程中异步对 sqlite 进行实际保存。长话短说,这对我们的性能有很大帮助。但是数据完整性呢?!

想象一下,如果我要这样做:

let coredataManager = CoreDataStack()
coredataManager.saveMainContext() // save is done asynchronously in background queue
coredataManager.mainManagedObjectedContext.fetch(fetchrequest) 

如何保证我的 fetch 读取的是最新和更新的结果?

如果我们以异步方式进行写入,那么在同一时间进行另一次读取是否有可能导致意外结果,即保存更改的结果可能存在或不存在?

编辑: 我对下面的代码进行了改进。我可以保存完成处理程序参数。但这并不能解决整个问题。如果我从其他地方的 mainQueue 发出 fetchRequest 却不知道同时发生了保存怎么办?

enum SaveStatus
    case noChanges
    case failure
    case success



func saveMainContext(completionHandler: (SaveStatus -> ())) 
    guard savingContext.hasChanges || mainManagedObjectedContext.hasChanges else 
        completionHandler(.noChanges)
        return
    

    mainManagedObjectedContext.performAndWait 
        do 
            try mainManagedObjectedContext.save()
        catch let error
            completionHandler(.failure)
            fatalError(error.localizedDescription)
        
    

    savingContext.perform 
        do 
            try self.savingContext.save()
            completionHandler(.succes)
        catch let error
            completionHandler(.failure)
            fatalError(error.localizedDescription)
        
    

【问题讨论】:

您可能不应该使用 fatalError() 以防保存失败,因为它会使应用程序崩溃。当 saveContext 失败时,几乎可以保证您有未保存的数据,并且崩溃将保证未保存的数据丢失。您使用 SaveStatus 枚举的想法很好,但是如果可能的话,completionHandler 应该对有关保存失败的信息进行智能处理.这是一个非常有趣的问题,对于复杂的应用程序来说很难解决。 【参考方案1】:

mainManagedObjectContext 的所有调用都是同步的,因此是阻塞的。如果您调用saveMainContext(),然后立即调用mainManagedObjectedContext.fetch(fetchrequest),则即使保存/获取请求来自不同的队列,获取请求也不会通过,直到保存请求完成(请参阅上面链接中有关 FIFO 的段落) .

当您执行提取请求时,您并没有从持久存储中提取 - 您是从刚刚更新的子容器中提取。您无需等待将更改提交到持久存储,因为您没有从那里访问数据。子容器将为您提供最新的更改。

子容器一个容器 - 它将保存您在内存中的最新更改(而不是存储在磁盘上 - 这是持久容器的工作)。

这里真正的问题是您的CoreDataStack 应该实现单例模式以防止实例化同一容器的多个版本(从技术上讲,它们仍然在同一个线程上并因此被序列化,但访问容器不会是线程安全的)。换句话说,每次实例化CoreDataStack(),都会创建一个新的savingContextmainManagedObjectedContext

相反,只实例化一次。

class CoreDataStack 

    var coordinator: NSPersistentStoreCoordinator

    public static let sharedInstance = CoreDataStack()

    private override init() 
        self.coordinator = NSPersistantStoreCoordinator()
    

    ...
    rest of your code here
    ...

然后这样调用:

CoreDataStack.sharedInstance.saveMainContext()

(参见this link re: '孩子是否拥有与父母相同的对象?')

孩子不会与父母同步的唯一情况是您有多个孩子访问同一个父母 - 但这里似乎不是这种情况。

【讨论】:

【参考方案2】:

该问题并非特定于核心数据。

这是经典的读写问题。

保护数据源的常用方法是使用串行队列访问您的数据源。否则是的,没有串行队列,您将遇到读写问题。

在以下示例中:

let coredataManager = CoreDataStack() // 1
coredataManager.saveMainContext() // 2 save is done asynchronously in background queue
coredataManager.mainManagedObjectedContext.fetch(fetchrequest) // 3

coredataManager 将从串行队列中访问。因此,即使第 2 行的写入是异步完成的,第 3 行的读取也必须等到 serial 队列被解除阻塞。

【讨论】:

以上是关于当保存在后台异步完成时,我应该如何保证从嵌套上下文中的不同线程获取结果是最新的?的主要内容,如果未能解决你的问题,请参考以下文章

writeToFile:atomically: 会阻止异步读取吗?

当 iphone 应用程序进入后台或前台状态时,如何保存数据?

如何使用 useEffect 正确地在数组中获取和保存数据

将应用程序推送到后台时,NSManagedObjectContext 不会获取或保存对象

从 Core Data 中删除和添加存储

在后台线程上安全保存 Core Data 托管对象上下文的正确方法?