Многопоточные данные ядра иногда возвращают нулевые свойства - PullRequest
0 голосов
/ 11 февраля 2019

Я новичок в основных данных.У меня есть приложение, которое использует основные данные в качестве локального хранилища.Запись и чтение из основных данных осуществляется фоновыми потоками.Хотя в целом это работает, в редких случаях получаемые данные неверны, т. Е. Свойства извлекаемой сущности равны 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 равен nil.

Вот мои функции для хранения данных:

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 голосов
/ 12 февраля 2019

Проблема с моим кодом была, по-видимому, условием состязания:
Пока поток «fetch» ​​извлекал записи основных данных и пытался присвоить атрибуты свойствам, поток «store» удалял записи.
По-видимому, это освободило объекты атрибута, так что nil было сохранено как свойство.
Я думал, что persistentContainer автоматически предотвратит это, но это не так.

Решение состоит в том, чтобы выполнить оба фоновых потока persistentContainer в параллельной последовательной очереди , поток «извлечения» синхронно и поток «хранения» асинхронно сбарьер .
Таким образом, одновременные выборки могут выполняться, пока хранилище ожидает завершения всех текущих выборок.

Параллельная последовательная очередь определяется как

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
0 голосов
/ 11 февраля 2019

Вы уверены, что name не ноль для всех запрошенных объектов?Просто используйте guard-let, чтобы избежать ! для необязательных переменных.Также ! это не безопасный способ развернуть необязательную переменную, особенно если вы не уверены в источнике данных.

...