I faced the same deadlock problem (which is quite common in SO) that occurs in several NSManagedObjectContexts scripts and multiple threads. In some controllers of my application, my application uses background streams to receive data from a web service, and in the same stream it saves it. In other cases, when it makes sense not to move forward without saving (for example, save values ββfrom the form when they click "Next"), the saving is performed in the main thread. AFAIK there should be nothing wrong with this in theory, but sometimes I can make a dead end when called
if (![moc save:&error])
... and this seems to always be in the background thread, while maintaining a dead end. This does not happen with every call; in fact itβs quite the opposite, I have to use my application for a couple of minutes, and then it will happen.
I read all the messages I could find, as well as Apple docs, etc., and I am sure that I am following the recommendations. To be specific, my understanding of working with multiple MOC / thread comes down to the following:
- Each thread must have its own MOC.
- A MOC stream must be created in this stream (not transferred from one stream to another).
- NSManagedObject cannot be passed, but NSManagedObjectID can, and you use the ID to inflate NSManagedObject with another MOC.
- Changes from one MOC must be merged with the other if they both use the same PersistentStoreCoordinator.
A back, I came across some code for the auxiliary MOC class on this SO stream and found that it was easy to understand and quite convenient to use, so all of my MOC interactions now happen through this. Here is my ManagedObjectContextHelper class:
#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 is a fragment of a multithreaded bit where it looks dead end:
NSManagedObjectID *parentObjectID = [parent objectID]; dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul); dispatch_async(queue, ^{
The error conflict in error seems to indicate that this is due to 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 tried to insert refreshObject calls as soon as I have the MOC, and the theory is that if this is the MOC we used before (for example, we used the MOC on the main thread before and probably this is the same as the helper class) , then possibly saving to another thread means that we need to explicitly update. But that didn't make any difference; it still blocks if I keep pressing for long enough.
Does anyone have any ideas?
Edit: if I have a breakpoint for all exceptions, the debugger automatically stops on the if (![moc save:&error]) line if (![moc save:&error]) , so the play / pause button is already paused and displays 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 actually blocks threads. Here is a screehshot of the status of both threads during a pause.