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 an NSInvocationOperation
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 calls updateWithAttributes: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 become NSDate
s). It also sets relationships.
From looking at your code i see you have 2 [childContext performBlockAndWait:^{ that are nested. Removing one of them should clear your issue in ios7. The code is already running in that thread you dont have to do it again.
Always check if you have any nested performBlocks for the same context. This has caused my app to deadlock before in ios7 and work in ios8
The way to check is when you see the deadlock, press pause in debugger and see what blocks all the threads are running. Look at that specific code and check for the nested blocks.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With