performBlockAndWait On Child Context with Private

2019-04-26 16:45发布

I have two NSManagedObjectContexts named importContext and childContext. childContextis the child of importContextand 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:

  1. updateItems: is called from an NSInvocationOperation in an operation queue, so it's off the main queue.

  2. 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:

enter image description here

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 NSDates). It also sets relationships.

2条回答
Juvenile、少年°
2楼-- · 2019-04-26 17:02

Who is calling -updateItems:? If that is coming in on the Main Queue you have an issue right there because you are blocking it.

Assuming that is not the case, can you share the thread stack from Xcode that shows the deadline? Specifically with the queues expanded and the main queue expanded?

I will update my answer once I get a good look at that stack.

Quellish is correct that you are not inserting into the child properly. Any activity on that child MOC should be inside of a -performBlock: or -performBlockAndWait:. I would expand the -performBlockAndWait: to cover the entire creation and decision for the object instead of just the save.

Update 1

What does -createWithAttributes:inManagedObjectContext:error: do? Seems like that method is doing something inappropriate. Maybe trying to force permanent IDs or something?

Update 2

As suspected, your -createWithAttributes:inManagedObjectContext:error: is your issue. When you call -objectWithID: you are causing a fetch to fire all the way down at the NSPersistentStoreCoordinator which in turn causes a lock.

Further, this method does not do anything helpful. There is absolutely no value in creating a context just to create an object and then immediately grab the object in another context. All harm, no good. Remove it completely and just create the object in the context where you are actually going to use it. Save or toss it from the context it is being used in.

Don't be clever.

查看更多
我欲成王,谁敢阻挡
3楼-- · 2019-04-26 17:19

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.

查看更多
登录 后发表回答