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

时间:2019-02-11 13:43:05

标签: swift multithreading core-data background-process

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

2 个答案:

答案 0 :(得分:0)

您确定对于所有请求的实体,sqlite3.InterfaceError: Error binding parameter 2 - probably unsupported type. 都不为零吗?只需使用guard-let避免使用import sqlite3, rsa db = sqlite3.connect('database.db') db.execute('drop table if exists user') db.execute('create table user (username text, password text, pubKey tuple, privKey tuple)') username = input("Input Username: ") password = input("Input Password: ") confirm = input("Confirm Password: ") (pubKey, privKey) = rsa.newkeys(512) if password == confirm: db.execute('insert into user (username, password, pubKey, privKey) values (?, ?, ?, ?)', (username, password, pubKey, privKey)) db.commit() else: quit() 作为可选变量。同样,name也不是解开可选变量的安全方法,尤其是在不确定数据源的情况下。

答案 1 :(得分:0)

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

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

并发串行队列定义为

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

编辑:
在以下获取和存储函数中,我将核心数据函数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  

,“ store”线程为

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