【问题标题】:Multi-threaded core data sometimes returns nil properties多线程核心数据有时会返回 nil 属性
【发布时间】:2019-02-11 13:43:04
【问题描述】:

我是核心数据的新手。我有一个使用核心数据作为本地存储的应用程序。写入和读取核心数据由后台线程完成。虽然这通常有效,但在极少数情况下获取的数据是错误的,即获取的实体的属性是nil
为了检查这种情况,我编写了一个启动 2 个异步线程的单元测试:一个从核心数据中连续获取,另一个通过首先删除所有数据,然后存储新数据来连续覆盖这些数据。
这个测试很快就引发了错误,但我不知道为什么。当然我猜这是一个多线程问题,但我不明白为什么,因为获取和删除+写入是在单个 persistentContainer 的单独托管上下文中完成的。
很抱歉下面的代码很长,虽然缩短了,但我认为没有它就无法识别问题。
非常欢迎任何帮助!

这是我获取数据的函数:

func fetchShoppingItems(completion: @escaping (Set<ShoppingItem>?, Error?) -> Void) {
    persistentContainer.performBackgroundTask { (managedContext) in 
        let fetchRequest: NSFetchRequest<CDShoppingItem> = CDShoppingItem.fetchRequest()
        do {
            let cdShoppingItems: [CDShoppingItem] = try managedContext.fetch(fetchRequest)
            for nextCdShoppingItem in cdShoppingItems {
                nextCdShoppingItem.managedObjectContext!.performAndWait {
                    let nextname = nextCdShoppingItem.name! // Here, sometimes name is nil
                } // performAndWait
            } // for all cdShoppingItems
            completion(nil, nil)
            return
        } catch let error as NSError {
            // error handling
            completion(nil, error)
            return
        } // fetch error
    } // performBackgroundTask
} // fetchShoppingItems

我已经评论了有时会使测试崩溃的行,因为namenil

这是我存储数据的函数:

func overwriteCD(shoppingItems: Set<ShoppingItem>,completion: @escaping () -> Void) {
    persistentContainer.performBackgroundTask { (managedContext) in 
        self.deleteAllCDRecords(managedContext: managedContext, in: "CDShoppingItem")
        let cdShoppingItemEntity = NSEntityDescription.entity(forEntityName: "CDShoppingItem",in: managedContext)!
        for nextShoppingItem in shoppingItems {
            let nextCdShoppingItem = CDShoppingItem(entity: cdShoppingItemEntity,insertInto: managedContext)
            nextCdShoppingItem.name = nextShoppingItem.name
        } // for all shopping items
        self.saveManagedContext(managedContext: managedContext)
        completion()
    } // performBackgroundTask
} // overwriteCD  

func deleteAllCDRecords(managedContext: NSManagedObjectContext, in entity: String) {
    let deleteFetch = NSFetchRequest<NSFetchRequestResult>(entityName: entity)
    let deleteRequest = NSBatchDeleteRequest(fetchRequest: deleteFetch)
    deleteRequest.resultType = .resultTypeObjectIDs
    do {
        let result = try managedContext.execute(deleteRequest) as? NSBatchDeleteResult
        let objectIDArray = result?.result as? [NSManagedObjectID]
        let changes = [NSDeletedObjectsKey: objectIDArray]
        NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes as [AnyHashable: Any], into: [managedContext])
    } catch let error as NSError {
        // error handling
    }
} // deleteAllCDRecords

func saveManagedContext(managedContext: NSManagedObjectContext) {
    if !managedContext.hasChanges { return }
    do {
        try managedContext.save()
    } catch let error as NSError {
        // error handling
    }
} // saveManagedContext

【问题讨论】:

    标签: swift multithreading core-data background-process


    【解决方案1】:

    您确定name 对所有请求的实体都不是 nil 吗?只需使用guard-let 来避免! 用于可选变量。另外! 解包可选变量不是安全的方法,尤其是在您无法确定数据来源的情况下。

    【讨论】:

    • 当我存储name 属性时,它肯定不是nil(该属性不是可选的)。这就是我使用 ! 的原因。否则,你是完全正确的。
    • @ReinhardMänner,好的,但是在崩溃时什么是 nil? name 还是实体?我认为如果name 为零,您应该在保存方法中寻找错误,而不是阅读。如果 nextShoppinItem.name 是可选的,您也可以尝试在 nextCdShoppingItem.name = nextShoppingItem.name 上添加默认值。例如:nextCdShoppingItem.name = nextShoppingItem.name ?? ""
    • 实体不是nil,而是属性。也许我应该修改 fetch 函数,以便在出现错误时第二次获取实体。这至少会显示读取或写入是否失败。
    【解决方案2】:

    我的代码的问题显然是竞争条件:
    当“fetch”线程获取核心数据记录并尝试将属性分配给属性时,“store”线程删除了记录。
    这显然释放了属性对象,因此nil 被存储为属性。
    我认为persistentContainer 会自动阻止这种情况,但事实并非如此。

    解决方案是在并发串行队列中执行persistentContainer的两个后台线程,同步执行“fetch”线程,异步执行“store”线程有屏障.
    因此,可以执行并发提取,而存储等待直到所有当前提取完成。

    并发串行队列定义为

    let localStoreQueue = DispatchQueue(label: "com.xxx.yyy.LocalStore.localStoreQueue", 
        attributes: .concurrent)  
    

    编辑:
    在下面的 fetch 和 store 函数中,我将核心数据函数 persistentContainer.performBackgroundTask 移动到了 localStoreQueue 中。如果它像我原来的答案一样在外面,localStoreQueue.async(flags: .barrier) 中的存储代码将设置一个新线程,因此在创建它的另一个线程中使用managedContext,这是一个核心数据多线程错误。

    “fetch”线程被修改为

    localStoreQueue.sync {
      self.persistentContainer.performBackgroundTask { (managedContext) in
        let fetchRequest: NSFetchRequest<CDShoppingItem> = CDShoppingItem.fetchRequest()
        //…
      } // performBackgroundTask  
    } // localStoreQueue.sync  
    

    和“存储”线程为

    localStoreQueue.async(flags: .barrier) {
      self.persistentContainer.performBackgroundTask { (managedContext) in
        self.deleteAllCDRecords(managedContext: managedContext, in: "CDShoppingItem")
        //…
      } // performBackgroundTask
    } // localStoreQueue.async
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-11-25
      • 1970-01-01
      • 1970-01-01
      • 2013-03-02
      • 2011-02-07
      相关资源
      最近更新 更多