Search code examples
core-dataconcurrencydeadlocknsmanagedobjectcontext

NSManagedObjectContext - Child Context causing deadlock


I have a parent - child - grandchild core data context setup in Core Data as below. Whenever I try execute a fetch request on the grandchild context, it causes a deadlock on the thread

- (NSManagedObjectContext *)defaultPrivateQueueContext
{
    if (!_defaultPrivateQueueContext) {
        _defaultPrivateQueueContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
        _defaultPrivateQueueContext.persistentStoreCoordinator = self.persistentStoreCoordinator;
    }
    return _defaultPrivateQueueContext;
}

- (NSManagedObjectContext *)mainThreadContext {
    if (!_mainThreadContext) {
        _mainThreadContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
        _mainThreadContext.parentContext = [self defaultPrivateQueueContext];
    }
    return _mainThreadContext;
}

+ (NSManagedObjectContext *)newPrivateQueueContext
{
    NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    context.parentContext = [[self sharedParliamentAPI] mainThreadContext];

    return context;
}

This is my code where it causes a deadlock (when trying to execute a fetch request):

- (void)fetchMenuItemsWithCompletion:(void (^) (BOOL success, NSString *message))completionBlock {
    NSMutableURLRequest *request = [APIHelper createNewRequestWithURLExtension:@"menuitems" httpMethodType:@"GET" parameters:nil];
    NSURLSession *session = [NSURLSession sessionWithConfiguration:self.sessionConfig];
    NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {

        NSObject *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
        if ([[json valueForKey:@"isSuccess"] boolValue]) {

            NSManagedObjectContext *defaultContext = self.defaultPrivateQueueContext;
            NSManagedObjectContext *privateQueueContext = [ParliamentAPI newPrivateQueueContext];

           [privateQueueContext performBlock:^{
                __block NSError *error;
                NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"MenuItem"];
                NSArray *fetchedRecords = [privateQueueContext executeFetchRequest:request error:&error];

                // do stuff with fetchedRecords

            }];
        } else {
            completionBlock([[json valueForKey:@"isSuccess"] boolValue], [json valueForKey:@"message"]);
        }
    }];
    [dataTask resume];
}

Solution

  • Your Core Data object structure looks like this:

    enter image description here

    Where you have a NSPrivateQueueConcurrencyType context as the root context connected to the persistent store coordinator, which has a child NSMainQueueConcurrencyType context, which in turn has a NSPrivateQueueConcurrencyType context. This structure is recommended by a lot of people who write on The IntarWebs.

    What is happening in your case is that the private queue context that is the main queue context is becoming busy, which is causing it's child to wait. Because the main queue context is using the main queue for all of it's work, it's not necessarily busy doing Core Data work when this happens (though this is still somewhat likely). The main queue does a lot of work other than Core Data, and all of those things will end up impacting the child any time it needs to communicate with the main queue context.

    Additionally, a context created with NSMainQueueConcurrencyType is allowed to execute Core Data operations without explicitly using performBlock: or performBlockAndWait:. For example, a main queue context can do this:

    NSArray *results    = nil;
    NSError *error      = nil;
    results = [mainQueueContext executeFetchRequest:fetchRequest error:&error];
    

    And is not required to do this:

    [mainQueueContext performBlock:^{
        NSArray *results    = nil;
        NSError *error      = nil;
        results = [mainQueueContext executeFetchRequest:fetchRequest error:&error];
    }];
    

    The difference here is that the first example without the performBlock: will block the main thread waiting for the result. The second, using performBlock:, is asynchronous and will not block - the fetch request will be scheduled for execution on the queue. Obviously, the first example can cause some contention in any child contexts.

    If your configuration had a NSMainQueueConcurrencyType context that was a child of another NSMainQueueConcurrencyType context, that would be... bad. It's almost guaranteed to deadlock in that configuration. The good news is, you don't have that issue!

    You can convert your code to using performBlock: with the NSMainQueueConcurrencyType context to mitigate this part of the problem. You can also use an NSPrivateQueueConcurrencyType in place of your main queue context - there is not much of a good reason to use a main queue context at all. NSFetchedResultsController can be used with a private queue context to do "background fetching".