Concurrent Core Data, Now Easier Than Ever

Core Data has a popular opinion of being hard to use, especially in concurrent environments. Why is that the case? First, it truly is complex because it solves a hard problem. Second, until WWDC16 Apple haven't really said how to best set up the Core Data stack. There were many options, each with its own issues, that we had to choose from.

That's why I'm super happy that things get clearer in iOS 10 with the introduction of NSPersistentContainer.

Before iOS 10

Core Data stack consists of a few main classes of objects: NSPersistentStore, NSPersistentStoreCoordinator, and NSManagedObjectContext. (If you're not familiar with them I recommend reading Core Data Overview by Daniel Eggert.)

Learning about these classes and the way they work together is a relatively large obstacle to someone who just wants to start writing code. That's why alternative databases began gaining popularity. One of them is Realm. Its multithreading model is the same as Core Data's. A better API makes a world of difference, though.

Apple seemed to notice that and answered in iOS 10.

NSPersistentContainer

The first new thing is NSPersistentContainer, encapsulating the whole Core Data stack setup. What we get is a simple interface thanks to which we can ignore the existence of NSPersistentStore and NSPersistentStoreCoordinator in most scenarios:

public var viewContext: NSManagedObjectContext { get }
public func newBackgroundContext() -> NSManagedObjectContext
public func performBackgroundTask(_ block: (NSManagedObjectContext) -> Swift.Void)

Our main thread-bounded operations use viewContext. Background work can be performed on a context returned by the factory method newBackgroundContext(). However, usage of performBackgroundTask(_:) is recommended because of the under-the-hood optimizations.

All contexts are independent of each other, there are no parent/child relationships here. To receive changes from other contexts we simply set automaticallyMergesChangesFromParent flag to true on a context. Despite its name (*FromParent*), it works correctly with the NSPersistentContainer setup in which contexts are siblings:

Core Data stack used by NSPersistentContainer (source: Apple)

It wasn't said or written explicitly but I think it's safe to assume that this is how Apple recommends setting up Core Data stack by default from iOS 10 onwards.

Before iOS 10 this setup had two flaws:

  • NSPersistentStoreCoordinator would lock and contexts would have to wait for each other. Main thread could become blocked.
  • It was possible to crash because of CoreData could not fulfill a fault for ... exception. It was happening because an object could be deleted by another context and accessed before our context got a notification of that change.

It turns out that both of these issues are gone in iOS 10 🎉. First one because of the internal changes in Core Data implementation that moved locking to SQLite level:

An NSManagedObjectContext (that is not nested) can now fetch and fault concurrently with other peer NSManagedObjectContext instances. For persistent stores using WAL journal_mode (the default in Core Data), the NSPersistentStoreCoordinator also supports 1 writer concurrently with multiple readers. For example, a UI context can fetch data concurrently with a single background context importing changes.

The second one thanks to Query Generations.

Query Generations

Query Generations allow us to take a consistent snapshot of a context at a given database transaction. You can think about them as checking out a commit in git. The minimal API is super easy to use. We just have to call:

try container.viewContext.setQueryGenerationFromToken(NSQueryGenerationToken.currentQueryGenerationToken)

and our viewContext is now pinned. We'll possibly have slightly stale data but we won't get any fault exceptions anymore! To move the context further we have to call setQueryGenerationFromToken(_:) again after doing some changes. The context will also be automatically advanced to the most recent version when it's saved, reset or when changes from another context are merged.

A full usage example can be found in the documentation. I recommend watching the whole WWDC session too.

Conclusion

It's really good to see so many improvements and clarifications about Core Data usage in iOS 10. They make working with it safer and more pleasant. I can't wait till I can use them in production.