Concurrency with Core Data

There are several situations in which performing operations with Core Data on a background thread or queue is beneficial, in particular if you want to ensure that your application’s user interface remains responsive while Core Data is undertaking a long-running task. If you do perform concurrent operations with Core Data, however, you need to take considerable care that object graphs do not get into an inconsistent state.

If you choose to use concurrency with Core Data, you also need to consider the application environment. For the most part, AppKit and UIKit are not thread safe; in particular, on OS X Cocoa bindings and controllers are not thread safe—if you are using these technologies, multi-threading may be complex.

Use Thread Confinement to Support Concurrency

The pattern recommended for concurrent programming with Core Data is thread confinement: each thread must have its own entirely private managed object context.

There are two possible ways to adopt the pattern:

  1. Create a separate managed object context for each thread and share a single persistent store coordinator.

    This is the typically-recommended approach.

  2. Create a separate managed object context and persistent store coordinator for each thread.

    This approach provides for greater concurrency at the expense of greater complexity (particularly if you need to communicate changes between different contexts) and increased memory usage.

You must create the managed context on the thread on which it will be used. If you use NSOperation, note that its init method is invoked on the same thread as the caller. You must not, therefore, create a managed object context for the queue in the queue’s init method, otherwise it is associated with the caller’s thread. Instead, you should create the context in main (for a serial queue) or start (for a concurrent queue).

Using thread confinement, you should not pass managed objects or managed object contexts between threads. To “pass” a managed object from one context another across thread boundaries, you either:

These create a local version of the managed object in the receiving context.

You can use the methods provided by NSFetchRequest to make working with data across threads easier and more efficient. For example, you can configure a fetch request to return just object IDs but also include the row data (and update the row cache)—this can be useful if you're just going to pass those object IDs from a background thread to another thread.

There is typically no need to use locks with managed objects or managed object contexts. However, if you use a single persistent store coordinator shared by multiple contexts and want to perform operations on it (for example, if you want to add a new store), or if you want to aggregate a number of operations in one context together as if a virtual single transaction, you should lock the persistent store coordinator.

Track Changes in Other Threads Using Notifications

Changes you make to a managed object in one context are not propagated to a corresponding managed object in a different context unless you either refetch or re-fault the object. If you need to track in one thread changes made to managed objects in another thread, there are two approaches you can take, both involving notifications. For the purposes of explanation, consider two threads, “A” and “B”, and suppose you want to propagate changes from B to A.

Typically, on thread A you register for the managed object context save notification, NSManagedObjectContextDidSaveNotification. When you receive the notification, its user info dictionary contains arrays with the managed objects that were inserted, deleted, and updated on thread B. Because the managed objects are associated with a different thread, however, you should not access them directly. Instead, you pass the notification as an argument to mergeChangesFromContextDidSaveNotification: (which you send to the context on thread A). Using this method, the context is able to safely merge the changes.

If you need finer-grained control, you can use the managed object context change notification, NSManagedObjectContextObjectsDidChangeNotification—the notification’s user info dictionary again contains arrays with the managed objects that were inserted, deleted, and updated. In this scenario, however, you register for the notification on thread B. When you receive the notification, the managed objects in the user info dictionary are associated with the same thread, so you can access their object IDs. You pass the object IDs to thread A by sending a suitable message to an object on thread A. Upon receipt, on thread A you can refetch the corresponding managed objects.

Note that the change notification is sent in NSManagedObjectContext’s processPendingChanges method. The main thread is tied into the event cycle for the application so that processPendingChanges is invoked automatically after every user event on contexts owned by the main thread. This is not the case for background threads—when the method is invoked depends on both the platform and the release version, so you should not rely on particular timing. If the secondary context is not on the main thread, you should call processPendingChanges yourself at appropriate junctures. (You need to establish your own notion of a work “cycle” for a background thread—for example, after every cluster of actions.)

Fetch in the Background for UI Responsiveness

The executeFetchRequest:error: method intrinsically scales its behavior appropriately for the hardware and work load. If necessary, the Core Data will create additional private threads to optimize fetching performance. You will not improve absolute fetching speed by creating background threads for the purpose. It may still be appropriate, however, to fetch in a background thread or queue to prevent your application’s user interface from blocking. This means that if a fetch is complicated or returns a large amount of data, you can return control to the user and display results as they arrive.

Following the thread confinement pattern, you use two managed object contexts associated with a single persistent store coordinator. You fetch in one managed object context on a background thread, and pass the object IDs of the fetched objects to another thread. In the second thread (typically the application's main thread, so that you can then display the results), you use the second context to fault in objects with those object IDs (you use objectWithID: to instantiate the object). (This technique is only useful if you are using an SQLite store, since data from binary and XML stores is read into memory immediately on open.)

Saving in a Background Thread is Error-prone

Asynchronous queues and threads do not prevent an application from quitting. (Specifically, all NSThread-based threads are “detached”—see the documentation for pthread for complete details—and a process runs only until all not-detached threads have exited.) If you perform a save operation in a background thread, therefore, it may be killed before it is able to complete. If you need to save on a background thread, you must write additional code such that the main thread prevents the application from quitting until all the save operation is complete.

If You Don’t Use Thread Containment

If you choose not to use the thread containment pattern—that is, if you try to pass managed objects or contexts between threads, and so on—you must be extremely careful about locking, and as a consequence you are likely to negate any benefit you may otherwise derive from multi-threading. You also need to consider that:

If you share a managed object context or a persistent store coordinator between threads, you must ensure that any method invocations are made from a thread-safe scope. For locking, you should use the NSLocking methods on managed object context and persistent store coordinator instead of implementing your own mutexes. These methods help provide contextual information to the framework about the application's intent—that is, in addition to providing a mutex, they help scope clusters of operations.

Typically you lock the context or coordinator using tryLock or lock. If you do this, the framework will ensure that what it does behind the scenes is also thread-safe. For example, if you create one context per thread, but all pointing to the same persistent store coordinator, Core Data takes care of accessing the coordinator in a thread-safe way (the lock and unlock methods of NSManagedObjectContext handle recursion).

If you lock (or successfully tryLock) a context, you must keep a strong reference to that context until you invoke unlock. If you don’t, in a multi-threaded environment, you may cause a deadlock.