lukecsmith.co.uk

Simple multi-threaded Core Data

Luke, 2 Oct 2018

Working with Core Data and using background threads still seems to instill fear with some developers. I’ve found it is easy, as long as you stick to a simple pattern, and never break some very important rules.

First, lets assume that you have a standard Core Data installation, that gives you access to a main NSManagedObjectContext :

NSManagedObjectContext {
    return persistentContainer.viewContext
}

If you have any large amount of data to process, you don’t want to be using that main context to do the work. Thats because the main context requires that you use the main thread when you access it. This is rule no.1 of Core Data threading : each context has a thread associated with it, and you cannot use that context on a different thread.

So for eg, this would be very wrong :

//our main context
let context = self.getContext()
//DONT DO THIS :
DispatchQueue.global().async {
    let fetchRequest: NSFetchRequest<CoreDataObject> = CoreDataObject.fetchRequest()
    context.fetch(request: fetchRequest)
}

The call to DispatchQueue.global() queued up the fetch task on a background thread. But using the main context on a thread other to the one it is supposed to be used on, will cause a crash eventually. Worse – it will crash in a way that gives you absolutely no clue about what went wrong.

The other rule is very similar – do not take objects from one context (and therefore, one thread), and try and use them on a different thread, or with a different context. As above then – you cannot take an NSManagedObject, then do some threading and process it on a different thread to the one you made it on.

So how do you do Core Data work in the background, so your UI remains responsive? First, you make a new context, which is a child context to your main context. And you set that context up to use a background thread at all times, like this :

let privateMOC = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
privateMOC.parent = context

The concurrencyType: .privateQueueConcurrencyType means that this context has its own private background thread. When you create this new context and set its parent as the main context, it effectively allows you to access all the data that you have in the main context, but on a background thread. Any changes made are done safely on this context, and then when you save it, the changes get pushed to the parent context, our main one.

This background thread, as its name suggests, is private, so cannot be accessed in any other way, apart from one of these two methods :

privateMOC.perform {
    //do some stuff on the background thread associated with this context
    //eg fetch some data, create some objects
}
// OR..
privateMOC.performAndWait {
    //do some stuff, but dont carry on beyond this block, until the code
    //in this block has finished executing (sync, not async)
}

So anything done within those perform parenthesise, will be done on a nice, un-jittery background thread. Your UI will remain lovely and responsive, no matter how much work you require is done in there. Clearly though, it isn’t yet going into your main Core Data context, so the rest of your code wont yet get access to the newly created or changed data. So how best to do that? First, Ive got a general save function, which will take any context, and save it :

func saveContext(forContext context: NSManagedObjectContext) {
        if context.hasChanges {
            context.performAndWait {
                do {
                    try context.save()
                } catch {
                    let nserror = error as NSError
                    print("Error when saving !!! \(nserror.localizedDescription)")
                    print("Callstack :")
                    for symbol: String in Thread.callStackSymbols {
                        print(" > \(symbol)")
                    }
                }
            }
        }
    }

As you can see, as a bonus, it prints out the call stack, should there have been an error while saving. It also uses that context.performAndWait function – which ensures that the save is done on the thread associated with that context, regardless of the thread that has called this function.

Heres the whole pattern I use, including save calls to both my private context, and to the main context :

let privateMOC = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
privateMOC.parent = context
privateMOC.performAndWait {
    //do some coredata stuff, but dont carry on beyond this block, until the code
    //in this block has finished executed (sync, not async)
    saveContext(forContext: privateMOC)
}
//now save the main context with the changes from the private context
saveContext(forContext: context)

So first, Im using the ‘performAndWait’ variety. That means that the block gets executed, but the code beyond the block does not get executed until the block has finished. At the end of the block, Im calling my save function, passing in the privateMOC. It will get saved on its own background thread, because thats what the save function does. This has the effect of updating the main context with those changes, BUT NOT the persistent store, YET.

Just after the block, I call saveContext again – for the main context. This pushes the changes that just came into this context from the private context, upto the persistent store. So thats its done – all my background work is now accessible to the rest of the app.

Following this pattern above, I find my apps are always responsive in terms of UI, and never get any core data errors or crashes, regardless of the amount of work I get various contexts to do simultaneously.

Tagged with: