我使用 coreData 作为持久存储。
为了读取数据,我使用(仅显示基本部分):
func fetchShoppingItems(completion: @escaping (Set<ShoppingItem>?, Error?) -> Void) {
persistentContainer.performBackgroundTask { (backgroundManagedContext) in
let fetchRequest: NSFetchRequest<CDShoppingItem> = CDShoppingItem.fetchRequest()
var shoppingItems: Set<ShoppingItem> = []
do {
let cdShoppingItems: [CDShoppingItem] = try backgroundManagedContext.fetch(fetchRequest)
for nextCdShoppingItem in cdShoppingItems {
nextCdShoppingItem.managedObjectContext!.performAndWait {
Thread.sleep(forTimeInterval: 0.1) // This seems to be required
let nextShoppingItem = ShoppingItem.init(name: nextCdShoppingItem.name!)
shoppingItems.insert(nextShoppingItem)
} // performAndWait
} // for all cdShoppingItems
completion(shoppingItems, nil)
return
} catch let error as NSError {
completion(nil, error)
return
} // fetch error
} // performBackgroundTask
} // fetchShoppingItems
为了测试 coreData 实现,我编写了一个单元测试,该测试创建多个并发写入和读取 coreData 的线程。
仅当指令
Thread.sleep(forTimeInterval: 0.1) // This seems to be required
插入到performAndWait
闭包中。
如果被注释掉,nextCdShoppingItem
通常会以 nil
属性读回,并且函数会因强制解包而崩溃。
我不确定 nextCdShoppingItem.managedObjectContext!.performAndWait
是否正确,或者我是否必须使用 backgroundManagedContext.performAndWait
,但使用 backgroundManagedContext
效果是一样的。
我不明白为什么在访问托管对象的属性之前插入一个小的延迟是避免该问题所必需的。
欢迎任何提示!
编辑:
我进一步调查了该问题,发现以下内容:
每次 nextCdShoppingItem
被后台线程(下面称为读取线程)读回为 nil
时,还有另一个后台线程在之后尝试保存自己的 ManagedContext其 ManagedContext 中的所有记录均已被删除(下面称为写入线程)。
显然,读取线程尝试获取刚刚被写入线程删除的记录。
所以问题肯定是多线程问题,我找到了解决方案(请参阅下面的答案)。
最佳答案
performAndWait 会将 block 添加到队列中并安排其运行,就像执行一样,但performAndWait 在 block 完成之前不会返回。由于您处于 cdShoppingItems
循环内,因此循环不会停止并等待 block 返回。通过添加线程 sleep ,您实际上会减慢循环速度,并为核心数据提供足够的时间来完成其获取。强制展开崩溃可能表明它丢失了 nextCdShoppingItem 引用。
我会考虑重构,您不需要在循环内查询核心数据。如果可能,请将 name
属性添加到 CDShoppingItem,这样您就不必获取它来构建 ShoppingItem 对象。
编辑:尽管我不知道你的确切用例,但尝试了重构:
func fetchShoppingItems(completion: @escaping (Set<ShoppingItem>?, Error?) -> Void) {
persistentContainer.performBackgroundTask { (backgroundManagedContext) in
let fetchRequest: NSFetchRequest<CDShoppingItem> = CDShoppingItem.fetchRequest()
do {
var shoppingItems: Set<ShoppingItem> = []
let cdShoppingItems: [CDShoppingItem] = try backgroundManagedContext.fetch(fetchRequest)
for nextCdShoppingItem in cdShoppingItems {
if let name = nextCdShoppingItem.name {
let nextShoppingItem = ShoppingItem.init(name: name)
shoppingItems.insert(nextShoppingItem)
}
}
completion(shoppingItems, nil)
} catch let error as NSError {
print("Error fetching CDShoppingItem: \(error)")
completion(nil, error)
} // fetch error
return
} // performBackgroundTask
} // fetchShoppingItems
关于ios - CoreData 多线程获取在访问属性之前需要很小的延迟,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/57083395/