It seems that in November, Apple updated both the NSManagedObjectContext Class Reference and the Core Data Programming Guide documents to explicitly bless serial GCD Dispatch Queues and NSOperationQueues as acceptable mechanisms for synchronising access to a
NSManagedObjectContext. But their advice seems ambiguous and possibly contradictory, and I want to make sure I've understood it properly.
Previously the accepted wisdom seemed to be that a
NSManagedObjectContext could only be accessed from the thread that created it, and that using a serial queue for synchronisation was not sufficient; although serial queues only perform one operation at a time, these operations can potentially be scheduled on different threads, and a MOC doesn't like that.
But now, from the programming guide, we have:
You can use threads, serial operation queues, or dispatch queues for concurrency. For the sake of conciseness, this article uses “thread” throughout to refer to any of these.
So far, so good (although their conflation of threads and queues is unhelpful). So I can safely use a single context per (serial) queue, instead of one per operation/block, right? Apple even has a visual depiction of this in the Core Data WWDC sessions.
But... where do you create the context for the queue? In the
NSManagedObjectContext documentation, Apple state:
[A context] assumes the default owner is the thread or queue that allocated it—this is determined by the thread that calls its init method. You should not, therefore, initialize a context on one thread then pass it to a different thread.
So now we have the idea of a
NSManagedObjectContext needing to know who its owner is. I'm assuming this means that the first operation to be executed in the queue should create the MOC and save a reference to it for the remaining operations to use.
Is this right? The only reason I'm hesitant is that the
NSManagedObjectContext article goes on to say:
Instead, you should pass a reference to a persistent store coordinator and have the receiving thread/queue create a new context derived from that. If you use NSOperation, you must create the context in main (for a serial queue) or start (for a concurrent queue).
Apple now seem to be conflating operations with the queues that schedule their execution. This does my head in, and makes me wonder if they really do want you to just create a new MOC for every operation after all. What am I missing?
The NSManagedObjectContext and any managed objects associated with it should be pinned to a single actor (thread, serialized queue, NSOperationQueue with max concurrency = 1).
This pattern is called thread confinement or isolation. There isn't a great phrase for (thread || serialized queue || NSOperationQueue with max concurrency = 1) so the documentation goes on to say "we'll just use 'thread' for the remainder of the Core Data doc when we mean any of those 3 ways of getting a serialized control flow"
If you create a MOC on one thread, and then use it on another, you have violated thread confinement by exposing the MOC object reference to two threads. Simple. Don't do it. Don't cross the streams.
We call out NSOperation explicitly because unlike threads & GCD, it has this odd issue where -init runs on the thread creating the NSOperation but -main runs on the thread running the NSOperation. It makes sense if you squint at it right, but it is not intuitive. If you create your MOC in -[NSOperation init], then NSOperation will helpfully violate thread confinement before your -main method even runs and you're hosed.
We actively discourage / deprecated using MOCs and threads in any other ways. While theoretically possible to do what bbum mentions, no one ever got that right. Everybody tripped up, forgot a necessary call to -lock in 1 place, "init runs where ?", or otherwise out-clevered themselves. With autorelease pools and the application event loop and the undo manager and cocoa bindings and KVO there are just so many ways for one thread to hold on to a reference to a MOC after you've tried to pass it elsewhere. It is far more difficult than even advanced Cocoa developers imagine until they start debugging. So that's not a very useful API.
The documentation changed to clarify and emphasize the thread confinement pattern as the only sane way to go. You should consider trying to be extra fancy using -lock and -unlock on NSManagedObjectContext to be (a) impossible and (b) de facto deprecated. It's not literally deprecated because the code works as well as it ever did. But your code using it is wrong.
Some people created MOCs on 1 thread, and passed them to another without calling -lock. That was never legal. The thread that created the MOC has always been the default owner of the MOC. This became a more frequent issue for MOCs created on the main thread. Main thread MOCs interact with the application's main event loop for undo, memory management, and some other reasons. On 10.6 and iOS 3, MOCs take more aggressive advantage of being owned by the main thread.
Although queues are not bound to specific threads, if you create a MOC within the context of a queue the right things will happen. Your obligation is to follow the public API.
If the queue is serialized, you may share the MOC with succeeding blocks that run on that queue.
So do not expose an NSManagedObjectContext* to more than one thread (actor, etc) under any circumstance. There is one ambiguity. You may pass the NSNotification* from the didSave notification to another thread's MOC's -mergeChangesFromContextDidSaveNotification: method.