4

I'm experiencing the same deadlock issue (that is quite common on SO) that occurs in the multiple NSManagedObjectContexts & multiple threads scenario. In some of my view controllers, my app uses background threads to get data from a web service, and in that same thread it saves it. In others, where it makes sense to not progress any further without saving (e.g. persist values from a form when they hit "Next"), the save is done on the main thread. AFAIK there should be nothing wrong with this in theory, but occasionally I can make the deadlock happen on a call to

if (![moc save:&error])

...and this seems to be always on the background thread's save when the deadlock occurs. It doesn't happen on every call; in fact it's quite the opposite, I have to use my app for a couple of minutes and then it'll happen.

I've read all the posts I could find as well as the Apple docs etc, and I'm sure I'm following the recommendations. To be specific, my understanding of working with multiple MOCs/threads boils down to:

  • Each thread must have its own MOC.
  • A thread's MOC must be created on that thread (not passed from one thread to another).
  • A NSManagedObject cannot be passed, but a NSManagedObjectID can, and you use the ID to inflate a NSManagedObject using a different MOC.
  • Changes from one MOC must be merged to another if they are both using the same PersistentStoreCoordinator.

A while back I came across some code for a MOC helper class on this SO thread and found that it was easily understandable and quite convenient to use, so all my MOC interaction is now thru that. Here is my ManagedObjectContextHelper class in its entirety:

#import "ManagedObjectContextHelper.h"

@implementation ManagedObjectContextHelper

+(void)initialize {
    [[NSNotificationCenter defaultCenter] addObserver:[self class]
                                             selector:@selector(threadExit:)
                                                 name:NSThreadWillExitNotification
                                               object:nil];
}

+(void)threadExit:(NSNotification *)aNotification {
    TDAppDelegate *delegate = (TDAppDelegate *)[[UIApplication sharedApplication] delegate];
    NSString *threadKey = [NSString stringWithFormat:@"%p", [NSThread currentThread]];
    NSMutableDictionary *managedObjectContexts = delegate.managedObjectContexts;

    [managedObjectContexts removeObjectForKey:threadKey];
}

+(NSManagedObjectContext *)managedObjectContext {
    TDAppDelegate *delegate = (TDAppDelegate *)[[UIApplication sharedApplication] delegate];
    NSManagedObjectContext *moc = delegate.managedObjectContext;

    NSThread *thread = [NSThread currentThread];

    if ([thread isMainThread]) {
        [moc setMergePolicy:NSErrorMergePolicy];
        return moc;
    }

    // a key to cache the context for the given thread
    NSString *threadKey = [NSString stringWithFormat:@"%p", thread];

    // delegate.managedObjectContexts is a mutable dictionary in the app delegate
    NSMutableDictionary *managedObjectContexts = delegate.managedObjectContexts;

    if ( [managedObjectContexts objectForKey:threadKey] == nil ) {
        // create a context for this thread
        NSManagedObjectContext *threadContext = [[NSManagedObjectContext alloc] init];
        [threadContext setPersistentStoreCoordinator:[moc persistentStoreCoordinator]];
        [threadContext setMergePolicy:NSErrorMergePolicy];
        // cache the context for this thread
        NSLog(@"Adding a new thread:%@", threadKey);
        [managedObjectContexts setObject:threadContext forKey:threadKey];
    }

    return [managedObjectContexts objectForKey:threadKey];
}

+(void)commit {
    // get the moc for this thread
    NSManagedObjectContext *moc = [self managedObjectContext];

    NSThread *thread = [NSThread currentThread];

    if ([thread isMainThread] == NO) {
        // only observe notifications other than the main thread
        [[NSNotificationCenter defaultCenter] addObserver:[self class]                                                 selector:@selector(contextDidSave:)
                                                     name:NSManagedObjectContextDidSaveNotification
                                                   object:moc];
    }

    NSError *error;
    if (![moc save:&error]) {
        NSLog(@"Failure is happening on %@ thread",[thread isMainThread]?@"main":@"other");

        NSArray* detailedErrors = [[error userInfo] objectForKey:NSDetailedErrorsKey];
        if(detailedErrors != nil && [detailedErrors count] > 0) {
            for(NSError* detailedError in detailedErrors) {
                NSLog(@"  DetailedError: %@", [detailedError userInfo]);
            }
        }
        NSLog(@"  %@", [error userInfo]);

    }

    if ([thread isMainThread] == NO) {
        [[NSNotificationCenter defaultCenter] removeObserver:[self class]                                                        name:NSManagedObjectContextDidSaveNotification
                                                      object:moc];
    }
}

+(void)contextDidSave:(NSNotification*)saveNotification {
    TDAppDelegate *delegate = (TDAppDelegate *)[[UIApplication sharedApplication] delegate];
    NSManagedObjectContext *moc = delegate.managedObjectContext;

    [moc performSelectorOnMainThread:@selector(mergeChangesFromContextDidSaveNotification:)
                          withObject:saveNotification
                       waitUntilDone:NO];
}
@end

Here's a snippet of the multi-threaded bit where it seems to deadlock:

NSManagedObjectID *parentObjectID = [parent objectID];

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
    dispatch_async(queue, ^{
        // GET BACKGROUND MOC
        NSManagedObjectContext *backgroundContext = [ManagedObjectContextHelper managedObjectContext];

        Parent *backgroundParent = (Parent*)[backgroundContext objectWithID:parentObjectID];
        // HIT THE WEBSERVICE AND PUT THE RESULTS IN THE PARENT OBJECT AND ITS CHILDREN, THEN SAVE...
[ManagedObjectContextHelper commit];

        dispatch_sync(dispatch_get_main_queue(), ^{

            NSManagedObjectContext *mainManagedObjectContext = [ManagedObjectContextHelper managedObjectContext];

            parent = (Parent*)[mainManagedObjectContext objectWithID:parentObjectID];
});
    });

The conflictList in the error seems to suggest that it's something to do with the ObjectID of the parent object:

    conflictList =     (
            "NSMergeConflict (0x856b130) for NSManagedObject (0x93a60e0) with objectID '0xb07a6c0 <x-coredata://B7371EA1-2532-4D2B-8F3A-E09B56CC04F3/Child/p4>' 
with oldVersion = 21 and newVersion = 22 
and old object snapshot = {\n    parent = \"0xb192280 <x-coredata://B7371EA1-2532-4D2B-8F3A-E09B56CC04F3/Parent/p3>\";\n    name = \"New Child\";\n    returnedChildId = 337046373;\n    time = 38;\n} 
and new cached row = {\n    parent = \"0x856b000 <x-coredata://B7371EA1-2532-4D2B-8F3A-E09B56CC04F3/Parent/p3>\";\n    name = \"New Child\";\n    returnedChildId = 337046373;\n    time = 38;\n}"
        );  

I've tried putting in refreshObject calls as soon as I've gotten hold of a MOC, with the theory being that if this is a MOC we've used before (e.g. we used an MOC on the main thread before and it's likely that this is the same one that the helper class will give us), then perhaps a save in another thread means that we need to explicitly refresh. But it didn't make any difference, it still deadlocks if I keep clicking long enough.

Does anyone have any ideas?

Edit: If I have a breakpoint set for All Exceptions, then the debugger pauses automatically on the if (![moc save:&error]) line, so the play/pause button is already paused and is showing the play triangle. If I disable the breakpoint for All Exceptions, then it actually logs the conflict and continues - probably because the merge policy is currently set to NSErrorMergePolicy - so I don't think it's actually deadlocking on the threads. Here's a screehshot of the state of both threads while it's paused.

Community
  • 1
  • 1
bobsmells
  • 1,359
  • 2
  • 11
  • 22
  • When your app deadlocks, in which method does it hang on the main thread? – lassej Apr 16 '13 at 16:47
  • @lassej Here's the printout of Thread1: libsystem_kernel.dylib`mach_msg_trap: 0x98030c18: movl $4294967265, %eax 0x98030c1d: calll 0x9803449a ; _sysenter_trap 0x98030c22: ret 0x98030c23: nop – bobsmells Apr 17 '13 at 11:33
  • I'm not sure that's what you need. Thread6 is on [NSManagedObjectContext save]. I'm not sure Thread1 is actually blocked, but it's a deadlock on the two threads' MOCs if I've understood properly. – bobsmells Apr 17 '13 at 11:35
  • If you start the app from xcode with debugger and you click the pause button after it deadlocked, you'll see the call stack like this: [callstack screenshot](http://imgur.com/cTgPszi). – lassej Apr 17 '13 at 14:23
  • see edit to post above – bobsmells Apr 17 '13 at 15:26
  • @bobsmells Expand the viewable part of the thread when you pause (after the deadlock) using the slider at the bottom of the window. Clues may be hidden after the call to `save:` – Nick Apr 19 '13 at 18:41
  • @Nick Here's what the thread looks like with the slider all the way to the right. I'm not sure what to deduce from that. https://docs.google.com/file/d/0B-QHmo7MUgGaWmlpQlBlRFZvTUE/edit?usp=sharing – bobsmells Apr 21 '13 at 12:44
  • After disabling the breakpoint on "All Exceptions", and setting both the main thread's MOC and all others created within the helper class to NSMergeByPropertyObjectTrumpMergePolicy, I thought that maybe the problem had gone away, but no, it's still there. I don't quite get how there can still be an exception if the MOCs have been given their "priority instructions" via the policy. – bobsmells Apr 21 '13 at 12:48

1 Answers1

3

I do not recommend your approach at all. First, unless you are confined to iOS4, you should be using the MOC concurrency type, and not the old method. Even under iOS 5 (which is broken for nested contexts) the performBlock approach is much more sound.

Also, note that dispatch_get_global_queue provides a concurrent queue, which can not be used for synchronization.

The details are found here: http://developer.apple.com/library/ios/#documentation/cocoa/conceptual/CoreData/Articles/cdConcurrency.html

Edit

You are trying to manage MOCs and threading manually. You can do it if you want, but there be dragons in your path. That's why the new way was created, to minimize the chance for errors in using Core Data across multiple threads. Anytime I see manual thread management with Core Data, I will always suggest to change as the first approach. That will get rid of most errors immediately.

I don't need to see much more than you manually mapping MOCs and threads to know that you are asking for trouble. Just re-read that documentation, and do it the right way (using performBlock).

Jody Hagins
  • 27,943
  • 6
  • 58
  • 87
  • Thanks @Jody Hagins, although I'm not clear on the detail of what you're saying. I've read that link, and AFAIK it's pretty much in line with what I'm doing (so I'm clearly missing something) - but is what I'm doing the "old method"? What is the difference with the MOC concurrency type? What IS the MOC concurrency type? – bobsmells Apr 22 '13 at 06:48
  • Ok, sorry, I understand about the MOC concurrency type, but I don't understand why this still doesn't work as is. See, regarding your point about the global queue & synchronisation, my application never has database operations running on multiple threads concurrently, so I think that shouldn't be an issue, should it? I only use GCD really so I can update the UI (e.g. progress bar) while doing a long web service hit. – bobsmells Apr 22 '13 at 13:04
  • Thanks, the penny is starting to drop...but the problem is that I need to use multi-threading (via GCD in my case) for a different reason - mainly the long-running web service call, so how do the performBlock and performBlockAndWait fit into that? Given that these are methods on the MOC, can I put my web service call inside performBlock and then persist the resultant data immediately after, still inside the block? What about my UI updates that depend on these data changes? Is it a case of having an outer nonMainMOC performBlock which has a mainMOC performBlock inside the outer? – bobsmells Apr 23 '13 at 03:53
  • You can do your web stuff wherever you want, in whichever thread(s) you want. When it comes to doing the CoreData stuff, make sure you create the MOCs with the proper concurrency type, and wrapped with `performBlock`. – Jody Hagins Apr 23 '13 at 04:06
  • I think what I'm looking for is a completion block to update the UI, as I'm familiar with in the GCD paradigm. It may be that processPendingChanges does what I want, although I can't find any examples of how this works with 2 (or more) threads...which bit does the updating of the UI? If the MOC inserts are on a background thread and performBlock is (necessarily) asynchronous, how does the main thread get notified? The Apple doco seems really vague on this. (/library/mac/#documentation/Cocoa/Reference/CoreDataFramework/Classes/NSManagedObjectContext_Class/NSManagedObjectContext.html) – bobsmells Apr 23 '13 at 06:58
  • The main thread gets notified when you notify it. If you have a MainQueue concurrency MOC, then you either save into it from a child context, or merge into it from the worker context. If you don't want to use a MainQueue MOC, then you prepare the data and use GCD to call the block on the main queue. Still... your use of Core Data in your code is just plain wrong. See my first response. – Jody Hagins Apr 23 '13 at 13:18
  • 1
    I completely rejigged my core data code to use performBlock and performBlockAndWait. Seems fine and much simpler. Thanks all for your help. – bobsmells May 02 '13 at 07:57
  • 1
    Would you mind sharing your revamped code? I seem to have a similar issue and I guess the issue comes from my lack of understanding of CoreData! – Julius Jul 12 '14 at 13:26
  • Can you look at related [question](http://stackoverflow.com/questions/41073296/core-data-concurrency-performblockandwait-nsmanagedobjectcontext-zombie)? – Neil Galiaskarov Dec 10 '16 at 09:10