Search code examples
swiftcore-dataconcurrencysynchronizationnsmanagedobjectcontext

How should I guarantee fetch results from a different thread in a nested contexts are up to date, when saves are done asynchronously in background?


I've read the following Behavior differences between performBlock: and performBlockAndWait:? But wasn't able to find an answer to my question.

The following code is picked up from an RayWenderlich video. Specifically at 10:05 the code is something like this:

class CoreDataStack {
    var coordinator : NSPersistentStoreCoordinator

    init(coordinator: NSPersistentStoreCoordinator){
        self.coordinator = coordinator
    }
    // private, parent, in background used for saving
    private lazy var savingContext : NSManagedObjectContext = {
        let moc = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
        moc.persistentStoreCoordinator = coordinator
        return moc
    }()

    lazy var mainManagedObjectedContext : NSManagedObjectContext = {
        let moc = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
        moc.parent = self.savingContext
        return moc
    }()

    func saveMainContext() {
        guard savingContext.hasChanges || mainManagedObjectedContext.hasChanges else {
            return
        }

        mainManagedObjectedContext.performAndWait {
            do {
                try mainManagedObjectedContext.save()
            }catch let error{
                fatalError(error.localizedDescription)
            }
        }

        savingContext.perform {
            do {
                try self.savingContext.save()
            }catch let error{
                fatalError(error.localizedDescription)
            }
        }
    }
}

From what I understand what happens is that the main context just passes the changes to its parent context which is a private, background context. It does this synchronously.

Then the parent, private context, does the actual saving against sqlite in a background thread asynchronously. Long story short this helps us a lot with performance. But what about data integrity?!

Imagine if I was to do this:

let coredataManager = CoreDataStack()
coredataManager.saveMainContext() // save is done asynchronously in background queue
coredataManager.mainManagedObjectedContext.fetch(fetchrequest) 

How can I guarantee that my fetch is reading the most recent and updated results?

If we do our writes asynchronously then isn't there a chance that another read at the same time could end up with unexpected results ie results of the save changes could or could not be there?

EDIT: I've made an improvement with the code below. I can make my save take in a completionHandler parameter. But that doesn't resolve the entire problem. What if I'm making a fetchRequest from a mainQueue somewhere else that isn't aware that a save is happening at the same time?

enum SaveStatus{
    case noChanges
    case failure
    case success
}


func saveMainContext(completionHandler: (SaveStatus -> ())) {
    guard savingContext.hasChanges || mainManagedObjectedContext.hasChanges else {
        completionHandler(.noChanges)
        return
    }

    mainManagedObjectedContext.performAndWait {
        do {
            try mainManagedObjectedContext.save()
        }catch let error{
            completionHandler(.failure)
            fatalError(error.localizedDescription)
        }
    }

    savingContext.perform {
        do {
            try self.savingContext.save()
            completionHandler(.succes)
        }catch let error{
            completionHandler(.failure)
            fatalError(error.localizedDescription)
        }
    }
}

Solution

  • The question isn't specific to core-data.

    It's the classic read-write question.

    The common approach with protecting a datasource is to access your datasource using a serial queue. Otherwise yeah without the serial queue you will have a read-write problem.

    In the following example:

    let coredataManager = CoreDataStack() // 1
    coredataManager.saveMainContext() // 2 save is done asynchronously in background queue
    coredataManager.mainManagedObjectedContext.fetch(fetchrequest) // 3
    

    coredataManager is to be accessed from a serial queue. So even if the write in the 2nd line is done asynchronously, the read at line 3, will have to wait until the serial queue is unblocked.