bobsmells
bobsmells

Reputation: 1359

CoreData deadlock with multiple threads

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:

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.

Upvotes: 4

Views: 2406

Answers (1)

Jody Hagins
Jody Hagins

Reputation: 28409

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).

Upvotes: 3

Related Questions