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
Thread 0 Crashed: Dispatch queue: com.apple.main-thread
0 com.apple.CoreData 0x00007fff8602a7c4 -[NSManagedObject(_NSInternalMethods) _newSnapshotForUndo__] + 356
1 com.apple.CoreData 0x00007fff8602a3da – [NSManagedObjectContext(_NSInternalChangeProcessing) _registerUndoForOperation:withObjects:withExtraArguments:] + 218
2 com.apple.CoreData 0x00007fff8602a2f2 -[NSManagedObjectContext(_NSInternalChangeProcessing) _registerUndoForModifiedObjects:] + 34
3 com.apple.CoreData 0x00007fff85ff9933 -[NSManagedObjectContext(_NSInternalChangeProcessing) _processRecentChanges:] + 1155
4 com.apple.CoreData 0x00007fff86035e72 -[NSManagedObjectContext processPendingChanges] + 18
5 com.apple.CoreData 0x00007fff8604ca1c – [NSManagedObjectContext(_NSInternalNotificationHandling) _processEndOfEventNotification:] + 108
6 com.apple.CoreData 0x00007fff86029c6e -[NSManagedObjectContext(_NSInternalChangeProcessing) _undoManagerCheckpoint:] + 30
7 com.apple.Foundation 0x00007fff8460784e _nsnote_callback + 167
8 com.apple.CoreFoundation 0x00007fff842d0a90 __CFXNotificationPost + 1008
9 com.apple.CoreFoundation 0x00007fff842bd008 _CFXNotificationPostNotification + 200
10 com.apple.Foundation 0x00007fff845fe7b8 -[NSNotificationCenter postNotificationName:object:userInfo:] + 101
11 com.apple.Foundation 0x00007fff8466f557 -[NSUndoManager _postCheckpointNotification] + 74
12 com.apple.Foundation 0x00007fff8466f3b9 -[NSUndoManager _endUndoGroupRemovingIfEmpty:] + 86
13 com.apple.Foundation 0x00007fff8461ea71 +[NSUndoManager(NSPrivate) _endTopLevelGroupings] + 455
14 com.apple.AppKit 0x00007fff82819945 -[NSApplication run] + 509
15 com.apple.AppKit 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:
- (main thread) fetch Core Data objects, convert them into simpler types, and create NSOperation objects.
- (background thread) process the data assigned to it and pass the result into the main thread.
- (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.”