Core Data and Concurrency

Introduction : 

Up to now, we've used a single managed object context, which we created in the CoreDataManager class. This works fine, but there will be times when one managed object context won't suffice.
What happens if you access the same managed object context from different threads? What do you expect happens? What happens if you pass a managed object from a background thread to the main thread? Let's start with the basics.

Concurrency Basics

Before we explore solutions for using Core Data in multithreaded applications, we need to know how Core Data behaves on multiple threads. The documentation is very clear about this. Core Data expects to be run on a single thread. Even though that thread doesn't have to be the main thread, Core Data was not designed to be accessed from different threads.
Core Data expects to be run on a single thread.
The Core Data team at Apple is not naive, though. It knows that a persistence framework needs to be accessible from multiple threads. A single thread, the main thread, may be fine for many applications. More complex applications need a robust, multithreaded persistence framework.
Before I show you how Core Data can be used across multiple threads, I lay out the basic rules for accessing Core Data in a multithreaded application.

Managed Objects

NSManagedObject instances should never be passed from one thread to another. If you need to pass a managed object from one thread to another, you use a managed object's objectID property.
The objectID property is of type NSManagedObjectID and uniquely identifies a record in the persistent store. A managed object context knows what to do when you hand it an NSManagedObjectID instance. There are three methods you need to know about:
  • object(with:)
  • existingObject(with:)
  • registeredObject(for:)
The first method, object(with:), returns a managed object that corresponds to the NSManagedObjectID instance. If the managed object context doesn't have a managed object for that object identifier, it asks the persistent store coordinator. This method always returns a managed object.
Know that object(with:) throws an exception if no record can be found for the object identifier it receives. For example, if the application deleted the record corresponding with the object identifier, Core Data is unable to hand your application the corresponding record. The result is an exception.
The existingObject(with:) method behaves in a similar fashion. The main difference is that the method throws an error if it cannot fetch the managed object corresponding to the object identifier.
The third method, registeredObject(for:), only returns a managed object if the record you're asking for is already registered with the managed object context. In other words, the return value is of type NSManagedObject?. The managed object context doesn't fetch the corresponding record from the persistent store if it cannot find it.
The object identifier of a record is similar, but not identical, to the primary key of a database record. It uniquely identifies the record and enables your application to fetch a particular record regardless of what thread the operation is performed on.
let objectID = managedObject.objectID

DispatchQueue.main.async {
    let managedObject = managedObjectContext?.object(with: objectID)
    ...
}
In the example, we ask a managed object context for the managed object that corresponds with objectID, an NSManagedObjectID instance. The managed object context first looks if a managed object with a corresponding object identifier is registered in the managed object context. If there isn't, the managed object is fetched or returned as a fault.
It's important to understand that a managed object context always expects to find a record if you give it an NSManagedObjectID instance. That is why object(with:) returns an object of type NSManagedObject, not NSManagedObject?.

Managed Object Context

Creating an NSManagedObjectContext instance is a cheap operation. You should never share managed object contexts between threads. This is a hard rule you shouldn't break. The NSManagedObjectContext class isn't thread safe. Plain and simple.
You should never share managed object contexts between threads. This is a hard rule you shouldn't break.

Persistent Store Coordinator

Even though the NSPersistentStoreCoordinator class isn't thread safe either, the class knows how to lock itself if multiple managed object contexts request access, even if these managed object contexts live and operate on different threads.
It's fine to use one persistent store coordinator, which is accessed by multiple managed object contexts from different threads. This makes Core Data concurrency a little bit easier ... a little bit.

Managing Concurrency

Core Data has come a long way and it used to be a nightmare to use Core Data in a multithreaded application. You still need to be careful when using Core Data on multiple threads, but it's become easier since iOS 6. Apple added a number of useful APIs to the Core Data framework to make your life as a developer easier.

Updating the Core Data Stack

Theory

Complex applications that heavily rely on Core Data can run into problems if changes of the main managed object context are written to the persistent store. Even on modern iOS devices, such operations can result in the main thread being blocked for a non-trivial amount of time. The user experiences this as the user interface freezing for a moment.
This can be avoided by slightly modifying the Core Data stack of the application. The approach I mostly use looks something like this.
Core Data and Concurrency | Core Data Stack
The managed object context linked to the persistent store coordinator isn't associated with the main thread. Instead, it lives and operates on a background thread. When the private managed object context saves its changes, the write operation is performed on that background thread.
The private managed object context has a child managed object context, which serves as the main managed object context of the application. The concept of parent and child managed object contexts is key in this scenario.
In most scenarios, a managed object context is associated with a persistent store coordinator. When such a managed object context saves its changes, it pushes them to the persistent store coordinator. The persistent store coordinator pushes the changes to the persistent store, a SQLite database for example.
A child managed object context doesn't have a reference to a persistent store coordinator. Instead, it keeps a reference to another managed object context, a parent managed object context. When a child managed object context saves its changes, it pushes them to the parent managed object context. In other words, when a child managed object context saves its changes, the persistent store coordinator is unaware of the save operation. It is only when the parent managed object context performs a save operation that the changes are pushed to the persistent store coordinator and subsequently to the persistent store.
Because no write operations are performed when a child managed object context saves its changes, the thread on which the operation is performed isn't blocked by a write operation. That is why the main managed object context of the application is the child managed object context of a managed object context that operates on a background thread.

Practice

It's time to put this into practice by updating the example we worked on in the previous tutorial. Download or clone the Notes application we created earlier from GitHub and open the project in Xcode. The only class we need to update is the CoreDataManager class.
git clone https://github.com/bartjacobs/ExploringTheFetchedResultsControllerDelegateProtocol

Creating a Private Managed Object Context

Most of the implementation remains unchanged. We start by creating a privateManagedObjectContext property of type NSManagedObjectContext. It's a private, lazy property.
private lazy var privateManagedObjectContext: NSManagedObjectContext = {
    // Initialize Managed Object Context
    var managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)

    // Configure Managed Object Context
    managedObjectContext.persistentStoreCoordinator = self.persistentStoreCoordinator

    return managedObjectContext
}()
Note that we initialize the managed object context by invoking init(concurrencyType:). The concurrency type tells Core Data how the managed object context should be managed from a concurrency perspective. What does that mean? The Core Data framework defines three concurrency types:
  • mainQueueConcurrencyType
  • confinementConcurrencyType
  • privateQueueConcurrencyType
The mainQueueConcurrencyType concurrency type associates the managed object context with the main queue. This is important if the managed object context is used in conjunction with view controllers or is linked to the application's user interface.
By setting the concurrency type to privateQueueConcurrencyType, the managed object context is given a private dispatch queue for performing its operations. The operations performed by the managed object context are not performed on the main thread. This is key.
The confinementConcurrencyType used to be the default. If you create a managed object context by invoking init(), the concurrency type is set to confinementConcurrencyType. However, as of iOS 9, the init() method of the NSManagedObjectContext class is deprecated. A managed object context should be created by invoking init(concurrencyType:), passing in either mainQueueConcurrencyType or privateQueueConcurrencyType.
Note that we set the persistentStoreCoordinator property of the private managed object context. This means that a save operation pushes the changes straight to the persistent store coordinator, which pushes the changes to the persistent store.

Updating the Main Managed Object Context

The next step is updating the implementation of the managedObjectContext property. With the above discussion in mind, the change is easy enough to understand.
private(set) lazy var managedObjectContext: NSManagedObjectContext = {
    let managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)

    managedObjectContext.parent = self.privateManagedObjectContext

    return managedObjectContext
}()
The managed object context is created by invoking init(concurrencyType:), passing in mainQueueConcurrencyType as the argument. Instead of setting the persistentStoreCoordinator property of the managed object context, we set its parent property to the private managed object context we created a moment ago.
This means that a save operation pushes changes from the main managed object context to the private managed object context. From a performance point of view, this is more than sufficient for the vast majority of applications that make use of Core Data.

To Save or Not to Save

You may be wondering when is the right time to save changes to the persistent store. And which managed object context should perform a save operation? Thanks to the updated Core Data stack, it's fine to perform save operations whenever necessary on the main managed object context. The main managed object context pushes the changes to its parent managed object context.
As for the private managed object context, it's sufficient to save any changes when the application is pushed to the background or when it is about to be terminated by the operating system. Open CoreDataManager.swift and add an import statement for the Foundation framework at the top.
import CoreData
import Foundation

final class CoreDataManager {

    ...

}
Update the designated initializer of the CoreDataManager class, init(modelName:), as shown below.
init(modelName: String) {
    self.modelName = modelName

    // Setup Notification Handling
    setupNotificationHandling()
}
In setupNotificationHandling(), we add the Core Data manager as an observer for two notification types:
  • Notification.Name.UIApplicationWillTerminate
  • Notification.Name.UIApplicationDidEnterBackground
// MARK: - Helper Methods

private func setupNotificationHandling() {
    let notificationCenter = NotificationCenter.default
    notificationCenter.addObserver(self, selector: #selector(CoreDataManager.saveChanges(_:)), name: Notification.Name.UIApplicationWillTerminate, object: nil)
    notificationCenter.addObserver(self, selector: #selector(CoreDataManager.saveChanges(_:)), name: Notification.Name.UIApplicationDidEnterBackground, object: nil)
}
The saveChanges(_:) method you see below is an internal instance method of the CoreDataManager class that:
  • pushes the changes from the main managed object context to the private managed object context
  • pushes the changes of the private managed object context to the persistent store coordinator
// MARK: - Notification Handling

@objc func saveChanges(_ notification: NSNotification) {
    managedObjectContext.perform {
        do {
            if self.managedObjectContext.hasChanges {
                try self.managedObjectContext.save()
            }
        } catch {
            let saveError = error as NSError
            print("Unable to Save Changes of Managed Object Context")
            print("\(saveError), \(saveError.localizedDescription)")
        }

        self.privateManagedObjectContext.perform {
            do {
                if self.privateManagedObjectContext.hasChanges {
                    try self.privateManagedObjectContext.save()
                }
            } catch {
                let saveError = error as NSError
                print("Unable to Save Changes of Private Managed Object Context")
                print("\(saveError), \(saveError.localizedDescription)")
            }
        }

    }
}
Notice that we first save the changes of the main managed object context before saving the changes of the private managed object context. This is important because we need to make sure the private managed object context includes the changes of its child managed object context.
For this reason, we save the private managed object context in the closure we pass to the perform(_:) method of the main managed object context. The perform(_:)method asynchronously executes the closure we hand it. In the closure we pass to the perform(_:) method, we push the changes of the main managed object context to the private managed object context by saving the main managed object context.
We then push the changes of the private managed object context to the persistent store coordinator by saving the changes of the private managed object context. To make sure we don't run into concurrency issues, we invoke perform(_:) for both save operations.
The perform(_:) method ensures the closure we pass it is executed on the dispatch queue of the managed object context. In other words, we are guaranteed we are playing by Core Data's concurrency rules. The NSManagedObjectContext class also defines performAndWait(_:). The only difference with perform(_:) is that performAndWait(_:) executes the closure synchronously hence the method's name.
Because the save() method is a throwing method, we wrap it in a do-catchstatement. Also note that we only invoke save() if the managed object context has any changes. We don't want to waste resources if there are no changes to save.
Don't worry if you are confused by the perform(_:) and performAndWait(_:) methods. We revisit these methods in the next tutorial. We also cover concurrency in detail in  :  Basic concept for Private context and Main context as well as Concurrency



Comments

Popular posts from this blog

iOS Architecture

Performance Tips for IOS Application

setNeedsLayout vs layoutIfNeeded Explained