I have two NSManagedObjectContext
s named importContext
and childContext
. childContext
is the child of importContext
and both of them are NSPrivateQueueConcurrencyType
.
To keep things off the main thread, I'm doing a bunch of work on the importContext
's queue. This work involves lots of fetches and saves, so it's convenient to wrap the whole thing inside a performBlockAndWait:
of the importContext
(it does need to by a synchronous operation because the code I have after the performBlockAndWait
depends on its results).
At some point during this work, I might need to create new managed objects from JSON results. These JSON values could be invalid and fail my validations, so after I create the objects, I need to be able to ditch them if they're no good. This is where childContext
comes in. I insert my new object into that, and if its JSON attributes end up not making sense, I ditch the childContext
.
The problem comes when I need to save childContext
. I expect it to have its own private queue, separate from its parent queue. However, this causes deadlock ONLY on iOS 7 (not iOS 8). When I run the same code on iOS 8 simulators and devices, the childContext
does create its own queue on a separate thread and does the save correctly.
It seems like when I am running iOS 7 the childContext
is trying to do save:
in the parent's queue, but the parent is waiting for its child which causes a deadlock. In iOS 8 this doesn't happen. Does anyone know why?
Here is the simplified code:
-(NSManagedObjectContext *)importContext
{
NSManagedObjectContext* moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
moc.persistentStoreCoordinator = [self storeCoordinator];
return moc;
}
-(void)updateItems:(NSArray*)ItemDescriptions
{
[self.importContext performBlockAndWait:^{
//get info and update
...
...
if(needToCreateNewItem){
NSManagedObjectContext* childContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
childContext.parentContext = self.importedContext;
//Insert and create new item
...
[childContext performBlockAndWait:^{
id newObject = [NSEntityDescription insertNewObjectForEntityForName:[self entityName]
inManagedObjectContext:childContext];
}];
...
// Do something with this object
if([newObject isReadyToSave])
__block NSError* e = nil;
__block BOOL saveSucceeded = NO;
[childContext performBlockAndWait:^{
saveSucceeded = [childContext save:&e]; // DEADLOCK ON iOS 7!!!!
}];
}
....
}
}];
}
An easy work-around is keeping the work on a separate dispatch queue (instead of the importContext
's queue), but the reason I'm asking this question is because I want to understand the underlying reason why this occurs. I'd think the child's save should just occur on its own queue.
UPDATE 1
Re. Marcus' questions:
updateItems:
is called from anNSInvocationOperation
in an operation queue, so it's off the main queue.On iOS 7, I can pause the app at anytime and view the stack and the managed object context's queue will be deadlocked:
(lldb) bt * thread #7: tid = 0xed07, 0x38546aa8 libsystem_kernel.dylib`semaphore_wait_trap + 8, queue = 'NSManagedObjectContext Queue' frame #0: 0x38546aa8 libsystem_kernel.dylib`semaphore_wait_trap + 8 frame #1: 0x385bbbac libsystem_platform.dylib`_os_semaphore_wait + 12 frame #2: 0x3848461a libdispatch.dylib`_dispatch_barrier_sync_f_slow + 138 frame #3: 0x2d4f3df2 CoreData`_perform + 102 frame #4: 0x2d4fe1ac CoreData`-[NSManagedObjectContext(_NestedContextSupport) executeRequest:withContext:error:] + 240 frame #5: 0x2d492f42 CoreData`-[NSManagedObjectContext save:] + 826 * frame #6: 0x000c1c96 DBDevApp`__69+[DBManagedObject createWithAttributes:inManagedObjectContext:error:]_block_invoke77(.block_descriptor=<unavailable>) + 118 at DBManagedObject.m:117 frame #7: 0x2d4f6934 CoreData`developerSubmittedBlockToNSManagedObjectContextPerform + 88 frame #8: 0x3847e81e libdispatch.dylib`_dispatch_client_callout + 22 frame #9: 0x384847ca libdispatch.dylib`_dispatch_barrier_sync_f_invoke + 26 frame #10: 0x2d4f6a72 CoreData`-[NSManagedObjectContext performBlockAndWait:] + 106 frame #11: 0x000c1916 DBDevApp`+[DBManagedObject createWithAttributes:inManagedObjectContext:error:](self=0x005c1790, _cmd=0x0054a033, attributes=0x188e context=0x17500800, error=0x02e68ae8) + 658 at DBManagedObject.m:116 frame #12: 0x000fe138 DBDevApp`-[DBAPIController createOrUpdateItems:withIDs:IDKeys:ofClass:amongExistingItems:withFindByIDPredicate:](self=0x17775de0, _cmd=0x0054de newItemDescriptions=0x188eada0, itemIDs=0x18849580, idKey=0x0058e290, class=0x005c1790, existingItems=0x1756b560, findByID=0x18849c80) + 2472 at DBAPIController.m:972 frame #13: 0x00100ca0 DBDevApp`__39-[DBAPIController updatePatientGroups:]_block_invoke(.block_descriptor=0x02e68ce0) + 476 at DBAPIController.m:1198 frame #14: 0x2d4f6934 CoreData`developerSubmittedBlockToNSManagedObjectContextPerform frame #15: 0x3847e81e libdispatch.dylib`_dispatch_client_callout + 22 frame #16: 0x384847ca libdispatch.dylib`_dispatch_barrier_sync_f_invoke + 26 frame #17: 0x2d4f6a72 CoreData`-[NSManagedObjectContext performBlockAndWait:] + 106 frame #18: 0x00100a96 DBDevApp`-[DBAPIController updatePatientGroups:](self=0x17775de0, _cmd=0x0054dfcd, groupsArray=0x188eada0) + 214 at DBAPIController.m:1191 frame #19: 0x2d721584 CoreFoundation`__invoking___ + 68 frame #20: 0x2d66c0da CoreFoundation`-[NSInvocation invoke] + 282 frame #21: 0x2e0f3d2c Foundation`-[NSInvocationOperation main] + 112 frame #22: 0x2e0515aa Foundation`-[__NSOperationInternal _start:] + 770 frame #23: 0x2e0f576c Foundation`__NSOQSchedule_f + 60 frame #24: 0x38484f10 libdispatch.dylib`_dispatch_queue_drain$VARIANT$mp + 488 frame #25: 0x38484c96 libdispatch.dylib`_dispatch_queue_invoke$VARIANT$mp + 42 frame #26: 0x38485a44 libdispatch.dylib`_dispatch_root_queue_drain + 76 frame #27: 0x38485d28 libdispatch.dylib`_dispatch_worker_thread2 + 56 frame #28: 0x385c0bd2 libsystem_pthread.dylib`_pthread_wqthread + 298
The code I showed above was a simplified version. The part where I create a new child context is inside a class called DBManagedObject
. Here's a screenshot of the whole stack:
Update 2 - Explaining DBManagedObject
DBManagedObject
is the base class for all my core data classes. It basically handles conversion to and from JSON-parsed dictionaries. It has 3 main methods: +createWithAttributes:inManagedObjectContext:error:
, -updateWithAttributes:error:
, and attributes
.
+createWithAttributes:inManagedObjectContext:error:
: creates a child context of the provided managed object context, inserts a new object in the child context and callsupdateWithAttributes:error:
on that object. If update is successful (ie. all the values we want to set on this object make sense), it saves the child context, obtains a reference to the new object in the MOC that came in as a parameter, and returns that reference:NSManagedObjectContext* childContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType]; childContext.parentContext = context; __block id newObject; [childContext performBlockAndWait:^{ newObject = [NSEntityDescription insertNewObjectForEntityForName:[self entityName] inManagedObjectContext:childContext]; }]; if ([newObject updateWithAttributes:attributes error:error]) { NSError* e = nil; if ([childContext save:&e]) { id parentContextObject = [context objectWithID:[(NSManagedObject*)newObject objectID]]; return parentContextObject; } else { if (error != NULL) { *error = e; } return nil; } } else return nil;
updateWithAttributes:error:
: does the heavy lifting of translating keys between the JSON keys to those I used in my data model as properties on the entities. (ie. 'first_name' becomes 'firstName'). It also formats the JSON values if needed (date strings becomeNSDate
s). It also sets relationships.