代码之家  ›  专栏  ›  技术社区  ›  Reinhard Männer

多线程核心数据有时返回零属性

  •  0
  • Reinhard Männer  · 技术社区  · 6 年前

    我不熟悉核心数据。我有一个应用程序使用核心数据作为本地存储。写入和读取核心数据是由后台线程完成的。虽然这通常有效,但在极少数情况下,提取的数据是错误的,即提取的实体的属性是 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
    

    我已经评论了这句话,它有时会使测试失败,因为 name .

    以下是我存储数据的功能:

    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
    
    2 回复  |  直到 6 年前
        1
  •  0
  •   Alexander Zakatnov    6 年前

    你确定吗 name 不是所有被请求实体都为零吗?用防护罩来避免 ! 对于可选变量。阿尔索 ! 打开可选变量是不安全的,尤其是当您不能确定数据源时。

        2
  •  0
  •   Reinhard Männer    6 年前

    我的代码的问题显然是种族问题:
    当fetch线程获取核心数据记录并尝试将属性分配给属性时,存储线程删除了这些记录。
    这显然释放了属性对象,因此 nil 存储为属性。
    我以为 persistentContainer 会自动阻止,但不会。

    解决方案是执行 持久容器 在一个 并发串行队列 ,同步获取线程,异步存储线程 有障碍 .
    因此,可以执行并发提取,而存储将等待所有当前提取完成。

    并发串行队列定义为

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

    提取线程修改为

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

    商店线程为

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