Teaching Core Data to dance with threads

Duncan Anderson

--

Warning: This post is quite a bit more technical than my usual ones. It’s aimed at Swift developers building iOS apps. If that’s you, dive in!

Core Data is Apple’s inbuilt data persistence API. It’s pretty good, but seems to be something that’s easy to tie yourself in knots with if you’re not careful.

My Core Data iOS app is multi-threaded and I bet yours is too.

Updating the user interface always happens on the main thread — that’s just how iOS works.

And if you load data into Core Data from across the network, that data arrives on a callback — which is a background thread.

But Core Data isn’t thread-safe. It’s not safe to access a single Core Data database from multiple threads, or to share Core Data objects across threads. Or, at least, it’s not safe to do such things without some clear designs that take into account the threading limitations we have to live with.

That’s the conundrum I was faced with in my app’s design. It’s something I struggled with. It’s something that the multitude of articles providing advice failed to help with — other than to further confuse me. The Core Data stack has evolved over time and there seems to be mixture of advice online — not all of which is up-to-date.

Now that I’ve resolved my issues and removed the confusion, I thought it worth sharing my findings in the hope that I can help others avoid such confusion. I must emphasise that what follows is what works for me; I am sure there will be other approaches that will also work. I hope that my practical summary of all the issues and solutions in one place fills the void I seemed to find whilst researching the topic myself.

Background and Main threads

As mentioned, an app that loads data across the network into a Core Data database and then displays that on a User Interface, is going to be multi-threaded. Without a threading design strategy, your app is going to crash at random points.

The good news is that implementing a Core Data threading strategy is actually very easy, once the basics are understood.

NSManagedObjectContext for each thread

We make Core Data calls through NSManagedObjectContext - which cannot be shared across threads. So we need to start by addressing this.

We can support multiple threads by having a separate NSManagedObjectContext for each thread. We then join those contexts to a single NSPersistentContainer, which looks after our physical data store, so we're able to talk to a single underlying data store across our threads. The big question, of course, is exactly how do we do this?

Avoid Merge Conflicts with a single background NSManagedObjecContext

Multiple NSManagedObjecContexts present a trap for the unsuspecting.

If we have multiple NSManagedObjectContexts writing to the same NSPersistentContainer, it's possible (probable) that we're going to get what's called merge conflicts - ie two different NSManagedObjectContexts changing the same object at the same time. It's possible to manage this by setting a Merge Policy; how Core Data should handle conflicts when they occur. But the complexity of such things, and the possibility of the merge policy not doing what I thought it did, makes me super-nervous.

I’d rather avoid managing merge conflicts if possible. And it turns out there’s a simple strategy to do so.

I create a single NSManagedObjectContext for all background Core Data processing and ensure that all background Core Data work is performed on a single thread. This ensures my updates are made one-at-a-time, rather than applied in parallel. Much safer!

Implementing a single background NSManagedObjectContext

Reading Apple’s documentation on the subject, it’s easy to get the impression that the simplest way to perform background work on Core Data is to do something like the following:

In fact, this is what I did at one point. But it turns out that every time you call container.performBackgroundTask() a new thread is spawned - so this is a fantastic way of creating multiple concurrent background threads all updating your data at the same time. That, I would suggest, is a recipe for disaster!

Instead, I have found a better strategy is to firstly setup the single background NSManagedObjectContext I mentioned, in my appDelegate:

Notice the newbackgroundContext.automaticallyMergesChangesFromParent = true - this ensures updates are propagated between this NSManagedObjectContext and my main context.

Then, when I want to do some background Core Data processing, I do something like the following (having passed my backgroundContext through the Dependency Injection pattern):

This ensures my background Core Data updates are all performed on that single thread and using the same NSManagedObjectContext. Those updates are forced to be executed in order and so I avoid the complexity of merge conflicts - and I'm thread safe.

Sharing data retrieved from Core Data across threads

Sometimes we need to be able to pass Core Data objects between threads. But we can’t do this, because Core Data objects themselves (ie the objects you are storing in your Core Data database) are also not thread safe.

But there’s a simple workaround. Every Core Data object has an objectID, which is thread safe. So instead of passing the object itself between threads, we can pass the objectID of the object.

The receiving thread then simply needs to retrieve the object, using it’s own NSManagedObjectContext, thus:

Where Object is the object type we're trying to pass around.

Make sure your ObjectIDs are permanent

When we create a new Core Data object it’s given a temporary objectID. We can test if this is the case with isTemporaryID.

If isTemporaryID returns true, we don't want to try passing it around; a temporary objectID won't exist if we try to access it from a different thread.

Again, the solution is simple — we just need to make sure to call save on the context before we do anything with our objectIDs. This bestows a non-temporary objectID onto our objects, meaning we're good-to-go passing them around our threads.

Summary

In summary, I’ve found the following strategy a good one for managing Core Data across threads:

  1. Create two NSManagedObjectContexts; one for main thread UI processing where I need to retrieve data to display and one for all background Core Data updates.
  2. Use self.backgroundContext.performAndWait { } to execute all background Core Data processing on a single backgroundContext and associated thread.
  3. Don’t pass Core Data objects around; instead pass their objectIDs and use context.existingObject(with: objectID) to retrieve the object when needed.
  4. Make sure to call save on the context before passing an objectID around.

--

--

Responses (3)

Write a response