61

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?

Nick Hutchinson
  • 5,044
  • 5
  • 33
  • 34

2 Answers2

64

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.

  • Ben
Ben
  • 846
  • 6
  • 3
  • 2
    When dealing with didSave, I've always taken the notification and passed it to the correct thread before applying it to the MOC. Is that unnecessary? For example, I have a MOC on the main thread, and one or more MOCs in NSOperations, and the main thread registers for the didSave notification. When it receives it, it passes the notification to the main thread and then gives it to the MOC. – Lily Ballard Jan 27 '11 at 03:54
  • 1
    The call to -mergeChangesFromContextDidSaveNotification must happen on the correct thread/queue. The NSNotification object itself can be retained from another thread's MOC and passed to it. A literal reading of the API contract might conclude that is illegal since the NSNotification's userInfo dictionary contains references to NSManagedObject from the MOC that is posting the notification. But the -mergeChangesFromContextDidSaveNotification: is special. Your code cannot troll through the NSNotification's userInfo on another thread. The framework code for mergeChanges is very very careful – Ben Feb 10 '11 at 09:08
  • 2
    Sorry, but I must disagree with your "But your code using it is wrong" comment. If the SDK class is not thread safe/cannot handle accesses from multiple threads, then the only thing that is "wrong" is the implementation of the SDK class. Don't defend a flawed implementation by implying that code which highlights the flaws in the implementation is itself incorrect. – aroth Oct 21 '11 at 03:02
  • 1
    Actually, @aroth, it really is virtually impossible for code using that style to be right. Furthermore, this isn't an issue of "thread safe" or "not thread safe" (or "supposed-to-be-but-isn't thread safe"). Core Data has always had a well-defined threading model - it's just that many people naiively assume they can just do things on multiple threads willy-nilly, or that no framework code ever accessses a context they create. Both are wrong. – Chris Hanson Jan 13 '12 at 02:39
  • 1
    @ChrisHanson I agree with you that "...your code is wrong" (because: this is the way the SDK is written; as developers, our code has to fit the API, not the other way around). But, to be fair: the SDK is "wrong" in the first place - no API should silently fail when accessed MT (at the least: it should throw exceptions, which CoreData *does not do*, instead it corrupts your database!). Other Apple API's which were single thread only, and with good reason, have historically been marked bugs and got fixed (e.g. in CoreAnimation and UIKit rendering). – Adam Jun 24 '13 at 11:03
  • 1
    Also, many thanks @Ben for the detailed explanation - I *wish* this answer was in the Apple docs. The current docs page on concurrency is too brief and vague, is still missing key details (and is nowhere near strong enough on the dangers of some common programming paradigms, that I've seen used on many live iOS projects :(). – Adam Jun 24 '13 at 11:06
  • Again, you're misinterpreting my and Ben's statements. We're not saying "NSManagedObjectContext doesn't support multiple threads" because that would be completely incorrect. My point in particular was that you cannot simply use whatever API you want however you want from any thread you want. Core Data has a well-defined threading model, that this is not the model you want it to have is not necessarily a bug. – Chris Hanson Aug 15 '13 at 05:01
  • 1
    Also, since this question was asked, managed object concurrency types were introduced to make writing multi-core Core Data apps much easier. You still have to adhere to the rules of the API: If you use private-queue concurrency for a context, you must funnel all interaction with a context and its objects through its -performBlock: or -performBlockAndWait: methods; in return, Core Data handles the serialization/locking for you. That's a huge improvement over thread-confinement concurrency — and I say that as one of the the only successful users of thread-confinement concurrency with Core Data. – Chris Hanson Aug 15 '13 at 05:05
  • So I take it Saul Mora's post at http://saulmora.com/2013/09/15/why-contextforcurrentthread-doesn-t-work-in-magicalrecord/ is wrong? It seems to contradict Ben here. – occulus Apr 01 '14 at 12:44
  • Saul Mora seems to be saying that thread identity is absolutely the problem if you try to access a MOC from a different thread to where it was created. But bbum below states it is serialisation of MOC access which is key, not thread identity. – occulus Apr 01 '14 at 12:47
11

Sounds like you had it right. If you're using threads, the thread that wants the context needs to create it. If you're using queues, the queue that wants the context should create it, most likely as the first block to execute on the queue. It sounds like the only confusing part is the bit about NSOperations. I think the confusion there is NSOperations don't provide any guarantee about what underlying thread/queue they run on, so it may not be safe to share a MOC between operations even if they all run on the same NSOperationQueue. An alternative explanation is that it's just confusing documentation.

To sum it up:

  • If you're using threads, create the MOC on the thread that wants it
  • If you're using GCD, create the MOC in the very first block executed on your serial queue
  • If you're using NSOperation, create the MOC inside of the NSOperation and don't share it between operations. This may be a bit paranoid, but NSOperation doesn't guarantee what underlying thread/queue it runs on.

Edit: According to bbum, the only real requirement is access needs to be serialized. This means that you can share a MOC across NSOperations as long as the operations are all added to the same queue, and the queue doesn't allow concurrent operations.

Lily Ballard
  • 182,031
  • 33
  • 381
  • 347
  • 3
    File a docs bug. The underlying meaning is rather simple; a MOC can be shared across threads/queues/etc **as long as only one thread/queue/etc is using it at a time**. I.e. all access to a MOC should be serialized in terms of execution. – bbum Jan 26 '11 at 05:13
  • @bbum Glad to hear it. I suspected it was as simple as that, but the fact that the documentation talked about the thread/queue owning the queue rather than just saying access needs to be serialized made me a bit paranoid. – Lily Ballard Jan 26 '11 at 05:18
  • @bbum -- is that always correct? I'm pretty sure I tried a design with a MOC that was initialized on the main thread, but then accessed only from a secondary thread. So all of my uses were from one thread at a time, but I got occasional crashes with what appeared to be bookkeeping tasks accessing the MOC on the main thread while I was working with it from the secondary thread. Also, this only seemed to happen on iOS devices, not in the simulator. – Daniel Dickison Jan 26 '11 at 14:22
  • 1
    @Daniel Interesting -- I'll ask an expert. – bbum Jan 26 '11 at 15:56
  • @bbum -- let us know what you hear, I'm very curious. (It's entirely possible that my experience was confounded by some other bug in my code -- I quickly refactored so the MOC was created on the accessing thread after running into that roadblock.) – Daniel Dickison Jan 26 '11 at 21:19
  • 3
    Actually, bbum is incorrect in this instance; you really should be creating the context on the thread that's going to use it. If, for example, you create one on the main thread it'll expect to process changes at the end of the main run loop. See -[NSManagedObjectContext processPendingChanges]. Or even better, just read Ben's response below... – Chris Hanson Jan 27 '11 at 09:55
  • 1
    A painful lesson I appear to have learnt from experience is that, under iOS 4 (but not 5) if you use a serial dispatch queue and your initial context creation is performed by `dispatch_sync` from the main thread then the context can end up being created from that thread (albeit though not on the main queue), causing the usual random thread confinement-related issues later on (ie, random meaningless SIGSEGVs, warnings about abstract properties that don't exist, etc). A switch to `dispatch_async` fixed the problem for me in practice, but doesn't fill me with confidence. Can anyone comment? – Tommy Feb 09 '12 at 13:19
  • @Tommy: If you dispatch_sync() from the main thread, the code will be executed on the main thread, and the resulting MOC will likely believe it belongs to the main thread instead of to a queue. – Lily Ballard Feb 09 '12 at 19:08
  • @KevinBallard while that does indeed seem to be true under iOS 4 (though fixed in 5 — presumably that checks the queue in preference or in addition to the thread), would you not agree that it contradicts the documentation's attempt to use 'thread' to mean any of a thread, a serial operation queue or a serial dispatch queue, as established in the box out at the top of 'Concurrency with Core Data'? – Tommy Feb 09 '12 at 19:53