Core Data and Multithreading

Apple’s documentation said that core data objects are not thread safe and if you want to multithread then you’ll need each thread to have its own NSManagedObjectContext instance and its object graph. Well, it turns out that you shouldn’t multithread at all with Core Data. At least you shouldn’t try to save in a background thread. When you use Core Data, there are other objects behind the scenes that interact with it and they may assume that they are dealing with a single threaded Core Data application. One of these is NSUndoManager.

This came into light when I was investigating and fixing seemingly random crashes in News Anchor. Practically all of these crashes happened in the main thread and during a refresh cycle — when the application fetches RSS feeds from the Internet and process them to make news channel episodes. Since this process requires network I/O and some heavy text processing, it really needs to be taken out from the main thread otherwise the app will be unresponsive. Initially I wrote this in a lengthy NSOperation object that have its own Core Data object graph (its own NSManagedObjectContext instance and related data object instances). Furthermore this operation performs two saves on its object context. I’ve done some tests and initially I found that it’s quite okay… until I released the application and people start buying it.

There were a lot of complaints about the application’s stability and the crash logs initially weren’t helpful. I got crashes such as these and the stack dump shows that it looks like Apple’s bug. Notice that the call was initiated by +[NSUndoManager(NSPrivate) _endtopLevelGroupings].

Thread 0 Crashed: Dispatch queue:
0 0x00007fff8602a7c4 -[NSManagedObject(_NSInternalMethods) _newSnapshotForUndo__] + 356
1 0x00007fff8602a3da –    [NSManagedObjectContext(_NSInternalChangeProcessing) _registerUndoForOperation:withObjects:withExtraArguments:] + 218
2 0x00007fff8602a2f2 -[NSManagedObjectContext(_NSInternalChangeProcessing) _registerUndoForModifiedObjects:] + 34
3 0x00007fff85ff9933 -[NSManagedObjectContext(_NSInternalChangeProcessing) _processRecentChanges:] + 1155
4 0x00007fff86035e72 -[NSManagedObjectContext processPendingChanges] + 18
5 0x00007fff8604ca1c –    [NSManagedObjectContext(_NSInternalNotificationHandling) _processEndOfEventNotification:] + 108
6 0x00007fff86029c6e -[NSManagedObjectContext(_NSInternalChangeProcessing) _undoManagerCheckpoint:] + 30
7 0x00007fff8460784e _nsnote_callback + 167
8 0x00007fff842d0a90 __CFXNotificationPost + 1008
9 0x00007fff842bd008 _CFXNotificationPostNotification + 200
10 0x00007fff845fe7b8 -[NSNotificationCenter postNotificationName:object:userInfo:] + 101
11 0x00007fff8466f557 -[NSUndoManager _postCheckpointNotification] + 74
12 0x00007fff8466f3b9 -[NSUndoManager _endUndoGroupRemovingIfEmpty:] + 86
13 0x00007fff8461ea71 +[NSUndoManager(NSPrivate) _endTopLevelGroupings] + 455
14 0x00007fff82819945 -[NSApplication run] + 509
15 0x00007fff828125f8 NSApplicationMain + 364

It may be Apple’s mistake but how can I blame Apple for this? This is my application after all and I need to get around it somehow. Besides end users won’t be bothered by this crash dump and blames me anyway.

So I re-factored the refresh operation and only perform core data operations in the main thread. The code was split into smaller chunks in multiple NSOperation classes that doesn’t operate on the Core Data objects directly — all data are converted into simpler values (e.g. NSString objects or container objects that solely contain simple types). All saves are done in the main thread. Using a concurrent operations really helps in this area since I can define NSOperation objects that runs only in the main thread but still I can schedule its dependency just like the other NSOperation objects that runs in background threads.

No multithreading? What about performance? Will the app display the spinning wheel more often? Actually there was no degradation in UI responsiveness at all. All network I/O are still done in background operations and the main thread is only involved intermittently — which doesn’t affect the GUI responsiveness at all (unless probably you’re a Borg drone, but drones will have direct link to the computer and won’t need a GUI ;-) ). Performance is actually better since there are no more calls to -[NSOperationQueue waitUntilAllOperationsAreFinished] (Initially for anything parallel I created multiple NSOperation objects, queue them, and wait for all of them to finish to perform post-processing. Now instead of waiting for operations in a queue, post-processing is done in another NSOperation object and instead of waiting, there is a dependency set-up between them).

This is the general pattern of the process:

  1. (main thread) fetch Core Data objects, convert them into simpler types, and create NSOperation objects.
  2. (background thread) process the data assigned to it and pass the result into the main thread.
  3. (main thread) perform post-processing, save, and launch other background NSOperation objects if needed.

The result? No more crashes. Most importantly, satisfied customers… or as one customer put it “I have come from a disgruntled customer to a satisfied (gruntled?) customer and will use News Anchor in my daily routine.”

Avoid App Review rules by distributing outside the Mac App Store!

Get my FREE cheat sheets to help you distribute real macOS applications directly to power users.

* indicates required

When you subscribe you’ll also get programming tips, business advices, and career rants from the trenches about twice a month. I respect your e-mail privacy.

Avoid Delays and Rejections when Submitting Your App to The Store!

Follow my FREE cheat sheets to design, develop, or even amend your app to deserve its virtual shelf space in the App Store.

* indicates required

When you subscribe you’ll also get programming tips, business advices, and career rants from the trenches about twice a month. I respect your e-mail privacy.

0 thoughts on “Core Data and Multithreading

Leave a Reply