如何从后台线程执行 CoreData context.count?

How to perform CoreData context.count from a background thread?

目前我对CoreData的实现如下

class CoreDataStack {
    static let INSTANCE = CoreDataStack()
    
    private init() {
    }
    
    private(set) lazy var persistentContainer: NSPersistentContainer = {
        let container = NSPersistentContainer(name: "xxx")
        
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                // This is a serious fatal error. We will just simply terminate the app, rather than using error_log.
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
        
        // So that when backgroundContext write to persistent store, container.viewContext will retrieve update from
        // persistent store.
        container.viewContext.automaticallyMergesChangesFromParent = true
        
        return container
    }()
    
    private(set) lazy var backgroundContext: NSManagedObjectContext = {
        let backgroundContext = persistentContainer.newBackgroundContext()

        backgroundContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
        
        return backgroundContext
    }()
}


class NSAttachmentRepository {
    static let INSTANCE = NSAttachmentRepository()
    
    private init() {
    }
    
    func isExist(_ name: String) -> Bool {
        let coreDataStack = CoreDataStack.INSTANCE
        let viewContext = coreDataStack.persistentContainer.viewContext
        
        let fetchRequest = NSFetchRequest<NSAttachment>(entityName: "NSAttachment")
        fetchRequest.fetchLimit =  1
        fetchRequest.predicate = NSPredicate(format: "name == %@", name)
        do {
            let count = try viewContext.count(for: fetchRequest)
            if count > 0 {
                return true
            }
        } catch {
            error_log(error)
        }
        
        return false
    }
}

我处理核心数据的策略是

  1. 要从主线程(UI 线程)执行非阻塞写入调用,我将使用 CoreDataStack.INSTANCE.backgroundContext
  2. 要从主线程(UI 线程)执行阻塞读取调用,我将使用 CoreDataStack.INSTANCE.persistentContainer.viewContext

这一直都很好,直到我需要执行以下操作

我们需要 运行 PHPickerViewControllerDelegateloadFileRepresentation 回调中的代码。如果我们在 loadFileRepresentation 回调中使用 Thread.isMainThread (returns false) 检查,它将在后台线程中执行。

当我在 Thread.isMainThread 为 false 的函数中执行调用 NSAttachmentRepository.INSTANCE.isExist(name) 时,我会遇到以下崩溃

CoreData`+[NSManagedObjectContext Multithreading_Violation_AllThatIsLeftToUsIsHonor]:

我尝试通过将代码从使用 coreDataStack.persistentContainer.viewContext 修改为 coreDataStack.backgroundContext

来“解决”问题
func isExist(_ name: String) -> Bool {
    let coreDataStack = CoreDataStack.INSTANCE
    ////let viewContext = coreDataStack.persistentContainer.viewContext
    let backgroundContext = coreDataStack.backgroundContext
    
    let fetchRequest = NSFetchRequest<NSAttachment>(entityName: "NSAttachment")
    fetchRequest.fetchLimit =  1
    fetchRequest.predicate = NSPredicate(format: "name == %@", name)
    do {
        ////let count = try viewContext.count(for: fetchRequest)
        let count = try backgroundContext.count(for: fetchRequest)
        if count > 0 {
            return true
        }
    } catch {
        error_log(error)
    }
    
    return false
}

但是,我仍然遇到同样的崩溃错误。

你知道我如何从后台线程执行 CoreData context.count 吗?

仅使用背景上下文是不够的。您需要在其自己的队列上使用该上下文。您检查过您不在主队列中 运行,但您可以在任何队列中,并且后台上下文仅适用于其中一个队列。当您在错误的队列上使用它时,您看到的错误消息是 Core Data 所说的。

任何时候使用 backgroundContext,您都需要将代码包装在对 performperformAndWait 的调用中,以确保您的代码在后台上下文的队列中运行。由于您的 isExist 函数是同步的,因此它需要使用 performAndWait 以便在返回之前可以得到结果。

感谢@Tom Harrington 的指点

下面是有关如何解决此类线程问题的代码片段。

func isExist(_ name: String) -> Bool {
    let coreDataStack = CoreDataStack.INSTANCE
    let viewContext = coreDataStack.persistentContainer.viewContext
    var result = false
    
    viewContext.performAndWait {
        let fetchRequest = NSFetchRequest<NSAttachment>(entityName: "NSAttachment")
        fetchRequest.fetchLimit =  1
        fetchRequest.predicate = NSPredicate(format: "name == %@", name)
        do {
            let count = try viewContext.count(for: fetchRequest)
            if count > 0 {
                result = true
            } else {
                result = false
            }
        } catch {
            error_log(error)
        }
    }

    return result
}