Примечание:
Этот пост не применяется, поскольку я фактически использую CoreData.
В этом посте последний ответ предлагает извлечь все элементы в новом фоновом потоке перед добавлением новых объектов, но это сделано в моем коде.
В этом посте предлагается разблокировать элемент перед сохранением его контекста, но это также сделано в моем коде.
Мое приложение использует CoreData для хранения объектов с именем shoppingItems
. Я написал класс CoreDataManager
, который инициализирует CoreData и имеет по существу одну функцию для перезаписи сохраненных в данный момент элементов и одну функцию для извлечения всех элементов. Обе функции работают в фоновом режиме, то есть в отдельном потоке.
Вот мой код (не относящиеся к делу части не учтены).
Я устанавливаю основные данные в главном потоке:
private lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: modelName)
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
})
return container
}()
Это функция записи:
func overwriteShoppingItems(_ shoppingItems: Set<ShoppingItem>, completion: @escaping (Error?) -> Void) {
let backgroundContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
let viewContext = self.persistentContainer.viewContext
backgroundContext.parent = viewContext
backgroundContext.performAndWait {
// Delete currently stored shopping items
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: CDEntityShoppingItem)
do {
let result = try backgroundContext.fetch(fetchRequest)
let resultData = result as! [NSManagedObject]
for object in resultData {
backgroundContext.delete(object)
}
if !shoppingItems.isEmpty {
// Save shopping items in managed context
let cdShoppingItemEntity = NSEntityDescription.entity(forEntityName: CDEntityShoppingItem, in: backgroundContext)!
for nextShoppingItem in shoppingItems {
let nextCdShoppingItem = CDShoppingItem(entity: cdShoppingItemEntity, insertInto: backgroundContext)
nextCdShoppingItem.name = nextShoppingItem.name
}
}
let saveError = self.saveManagedContext(managedContext: backgroundContext)
completion(saveError)
} catch let error as NSError {
completion(error)
}
}
}
func saveManagedContext(managedContext: NSManagedObjectContext) -> Error? {
if !managedContext.hasChanges { return nil }
do {
try managedContext.save()
return nil
} catch let error as NSError {
return error
}
}
И это функция выборки:
func fetchShoppingItems(completion: @escaping (Set<ShoppingItem>?, Error?) -> Void) {
let backgroundContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
let viewContext = self.persistentContainer.viewContext
backgroundContext.parent = viewContext
backgroundContext.performAndWait {
let fetchRequest: NSFetchRequest<CDShoppingItem> = CDShoppingItem.fetchRequest()
do {
let cdShoppingItems: [CDShoppingItem] = try backgroundContext.fetch(fetchRequest)
guard !cdShoppingItems.isEmpty else {
completion([], nil)
return
}
for nextCdShoppingItem in cdShoppingItems {
}
completion(shoppingItems, nil)
} catch let error as NSError {
completion(nil, error)
}
}
}
При нормальной работе код работает.
Проблема:
Я также написал модульный тест, который пытается вызвать проблемы с многопоточностью. Этот тест использует параллельную очередь отправки:
let concurrentReadWriteQueue = DispatchQueue(label: „xxx.test_coreDataMultithreading", attributes: .concurrent)
Таймер определяет время теста.
В схеме для испытаний я установил аргументы -com.apple.CoreData.Logging.stderr 1
и -com.apple.CoreData.ConcurrencyDebug 1
.
Во время теста overwriteShoppingItems
и fetchShoppingItems
многократно вставляются в очередь и выполняются одновременно.
Этот модульный тест выполняет несколько операций чтения и записи, прежде чем остановиться на строке
let itemName = nextCdShoppingItem.name!
потому что nextCdShoppingItem.name
- это nil
, что никогда не должно происходить, потому что я никогда не храню nil
.
Непосредственно перед аварией регистрируется следующее:
CoreData: error: API Misuse: Attempt to serialize store access on non-owning coordinator (PSC = 0x600000e6c980, store PSC = 0x0)
Если я делаю только выборки или только записи, предупреждение CoreData регистрируется , а не . Таким образом, это определенно является проблемой многопоточности.
Однако CoreData.ConcurrencyDebug
не обнаруживает его.
Похоже, что во время операции выборки в одном потоке другой поток удаляет текущий выбранный элемент, поэтому его свойства считываются как nil
.
Но этого не должно происходить, потому что выборки и сохранения выполняются с backgroundContext.performAndWait
, то есть поочередно.
А трассировка стека показывает, что только один поток обращается к CoreData: Thread 3 Queue : NSManagedObjectContext 0x600003c8c000 (serial)
Мои вопросы:
- Что я неправильно использую API-интерфейс CoreData и как это сделать правильно?
- Почему элемент иногда читается как ноль?
РЕДАКТИРОВАТЬ:
Может быть, это помогает выявить проблему: когда я закомментирую backgroundContext.delete(object)
в overwriteShoppingItems
, ошибка больше не регистрируется, и ни один элемент не выбирается как nil
.