I'm a software developer based in the UK. I am blogging regularly about software development & Apple

Asynchronous Core Data Requests

With iOS8 and iOS9 Apple extended its CoreData API with asynchronous functions. In this blog I want to take a closer look at these functions. But before that I'd like to address a few questions first.
Let's start with why Apple added asynchronous functions if performblock already is asynchronous? While performblock is asynchronous, the actual operation e.g. a fetch request on the context is not. What performBlock does is queue up an operation onto a queue. This happens because operations on a NSManagedObjectContext are not thread safe. When the actual operation e.g. the aforementioned fetch request occurs, the call blocks any further operation on the context until the requested data from the persistent store has been retrieved.

So you might ask what do we gain then with asynchronous functions?
Well, first of all NSMangedObjectContext requests don't block any other operation on the context any more. So operations like fetching some dataset A and then updating some dataset B, don't interfere with each other provided those datasets are disjoint. If they aren't data operations still can be executed independently from each other but common data needs to be reconciled after that. More about that later.
Then because of the nature of those operations, they are faster. In my tests with about 800 data rows they were faster by a factor 2 to 3. This is because data can be deleted or updated directly on the persistent store. Prior to iOS8 data had to retrieved first from the persistent store, updated in the managedObjectContext before it had to be written back again to update the store.
Last but not least, asynchronous fetch requests can be cancelled. So if in the past you had to deal with large requests blocking your NSManagedObjectContext, you have the option now to cancel those request.

This is great you say and then you start to wonder if there are any disadvantages. Well, truth be told there are a few.
First of all, not all asynchronous operations are available with every NSPersistentStore. So far all operations are available with an SQL persistent store but only asynchronous fetch requests are available in the in-memory persistent Store.
Secondly, changes in the persistent store made by asynchronous update or delete requests are not propagated to contexts that are connected to them. So data needs to be reconciled.
Another disadvantage is that CoreData validation rules won't be enforced. So there is potential to put a data model into an inconsistent state.
Apart from that asynchronous operation only work on NSManagedObjectContext of type PrivateQueueConcurrencyType and MainQueueConcurrencyType. ConfinementConcurrencyType by the way has been deprecated with iOS9. 
Lastly, not all operations are available on iOS8. The NSBatchDeleteRequest had been added in iOS9. Special care needs to be taken here for iOS8.

So which asynchronous operations are available? All in all there are three asynchronous operations:

  • asynchronous fetch request via NSAsynchronousFetchRequest
  • asynchronous update request via NSBatchUpdateRequest
  • asynchronous delete request via NSBatchDeleteRequest

Let's take a closer look at each of them individually,

NSAsynchronousFetchRequest

Asynchronous fetch requests are based on NSFetchRequest. Using them as you can see in the code snippet below is straight forward.

let fetchRequest = NSFetchRequest(entityName:"AASWeatherData")
fetchRequest.includesPropertyValues = true
let asyncRequest = NSAsynchronousFetchRequest(fetchRequest: fetchRequest) { results in
    if let results = results.finalResult {
// Do something
    }
_ = try! managedObjectContext.executeRequest(asyncRequest)

Like with synchronous fetch requests, the fetch request itself needs to be setup first. After that create an instance of NSAsynchronousFetchRequest by providing the fetch request along with a completion handler. Then provide that instance to the new API method executeRequest.
In my tests on an iPhone 6 asynchronous fetch requests were a little slower than their synchronous counterpart. But try it out yourself. You can find my test application here.

NSBatchUpdateRequest

Update requests are setup similar to NSFetchRequests. Instantiate a NSBatchUpdateRequest and provide the predicate and properties to update. Due to their asynchronous nature, the result type needs to be specified as well. You can choose between the actual number of updated data rows and the object ids of the updated models which can be used in a subsequent fetch request. The changes in the persistent store are not propagated to the NSManagedObjectContext as I mentioned before. So you have to reconcile the data if need be. If you would like to know when an update request completes you need to setup an observer on NSProgress and watch out for changes on its completedUnitCount property. You definitely have to do this if you would like to know the object ids or the number of updated models. I am going to dedicate another blog post to NSProgress since its use and its workings weren't that obvious to me. Let me just say that NSProgress works with thread local storage. The update request checks if a NSProgress has been setup and uses it to report progress and completion of the underlying operation.
In my tests batch update operations were considerable faster compared to a synchronous update operation.

let asyncRequest = NSBatchUpdateRequest(entityName: "AASWeatherData")
asyncRequest.resultType = .UpdatedObjectIDsResultType
asyncRequest.propertiesToUpdate = [ "location" : "London" ]
asyncRequest.predicate = NSPredicate(format: "location == %@","heathrow")
let progress = NSProgress(totalUnitCount: 1)
progress.becomeCurrentWithPendingUnitCount(1)
let batchUpdateResult = try! managedObjectContext.executeRequest(asyncRequest) as! NSBatchUpdateResult
self.asyncBatchUpdateObserver = AASKeyValueObserver(target: progress, keyPath: "completedUnitCount") { keyPath, dict in
    let completedUnitCount = dict![NSKeyValueChangeNewKey] as! Int
    if (completedUnitCount == 1)
    {
// Do something       
    }
}
progress.resignCurrent()

NSBatchDeleteRequest

A delete request is setup similar to an update request. The only difference is instantiating a class of type NSBatchDeleteRequest. Like with the update request you have to reconcile the objects in the used managedObjectContext with the persistent store.  Apart from that make sure you are targeting iOS9+ since the request is not available prior to this version.

In my tests with about 800 data rows the deletion was like the update considerably faster than the synchronous equivalent. 

let fetchRequest = NSFetchRequest(entityName:"AASWeatherData")
fetchRequest.includesPropertyValues = true
fetchRequest.predicate = NSPredicate(format: "location == %@","heathrow")
let asyncRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
asyncRequest.resultType = .ResultTypeCount
let progress = NSProgress(totalUnitCount: 1)
progress.becomeCurrentWithPendingUnitCount(1)
let batchUpdateResult = try! managedObjectContext.executeRequest(asyncRequest) as! NSBatchDeleteResult
self.asyncBatchUpdateObserver = AASKeyValueObserver(target: progress, keyPath: "completedUnitCount") { keyPath, dict in
    let completedUnitCount = dict![NSKeyValueChangeNewKey] as! Int
    if (completedUnitCount == 1)
    {
// Do something
    }
}
progress.resignCurrent()

Cancellation

A topic which I left out so far is cancellation. Cancellation works the same with every asynchronous request. This following code snippet shows the cancellation of a fetch request.

let fetchRequest = NSFetchRequest(entityName:"AASWeatherData")
fetchRequest.includesPropertyValues = true
let asyncRequest = NSAsynchronousFetchRequest(fetchRequest: fetchRequest) { results in
    let cancelled = results.progress?.cancelled
    if (cancelled ?? false)
    {
 // Do something
    }
}
let progress = NSProgress(totalUnitCount: 1)
progress.becomeCurrentWithPendingUnitCount(1)
let request = try! managedObjectContext.executeRequest(asyncRequest) as! NSPersistentStoreAsynchronousResult
progress.resignCurrent()
// A little later ...
request.progress!.cancel()


A request is cancelled via its associated NSProgress.  Each instance of NSProgress has a cancel() method via which any linked operation can be cancelled. In the example above the cancellation was addressed in the completion handler of the NSAsynchronousFetchRequest.  But it can as well be addressed in NSProgress' cancellationHandler.

Conclusion

The new asynchronous API is definitely beneficial when handling large data sets. Handling completion of update and deletion request via KVO on NSProgress makes it however quite cumbersome to use. Why Apple didn't just provide a completion handler with NSBatchDeleteRequest and NSBatchUpdateRequest is something I don't understand. They could even have added one to NSProgress. NSProgress already has a cancellationHandler and a resumeHandler. A completionHandler wouldn't definitely be out of place here.  This cumbersome code unfortunately leads to even more code (compared to the synchronous version). I hope that we see some improvements here in the near future.
In the table below I summarized my measurements of asynchronous and synchronous core data requests. The requests addressed about 800 rows in the persistent store. It shows that asynchronous requests like update and deletion are considerably faster. This however comes with a burden. The additional care that must be taken in regards to validation and reconciliation for update and delete requests makes them only a viable alternative where large data sets need to be handled.  

 Synchronous & Asynchronous request times with 800 affected rows on an iPhone 6 with a SQL persistent store

Synchronous & Asynchronous request times with 800 affected rows on an iPhone 6 with a SQL persistent store

Working with NSProgress

QoS with Dispatch Queues