6
votes

I am using nested contexts pattern to support multithreaded work with CoreData. I have CoredDataManager singleton class and the inits of contexts are:

self.masterContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
self.masterContext.persistentStoreCoordinator = self.persistentStoreCoordinator;

self.mainContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
self.mainContext.parentContext = self.masterContext;

For each insert operation on response from web service I use API of my CoreDataManager to get new managed context:

- (NSManagedObjectContext *)newManagedObjectContext {
    NSManagedObjectContext *workerContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    workerContext.parentContext = self.mainContext;

    return workerContext;
}

It looks something like (PlayerView class is subclass of NSManagedObject class):

[PlayerView insertIfNeededByUniqueKey:@"playerViewId" value:playerViewId inBackgroundWithCompletionBlock:^(NSManagedObjectContext *context, PlayerView *playerView) {
    playerView.playerViewId = playerViewId;
    playerView.username = playerViewDictionary[@"name"];

    [context saveContextWithCompletionBlock:^{
         //do something
    } onMainThread:NO];//block invocation on background thread
}];

saveContextWithCompletionBlock method is implemented in NSManagedObjectContext category:

- (void)saveContextWithCompletionBlock:(SaveContextBlock)completionBlock onMainThread:(BOOL)onMainThread {
    __block NSError *error = nil;

    if (self.hasChanges) {
        [self performBlock:^{
            [self save:&error];

            if (error) {
                @throw [NSException exceptionWithName:NSUndefinedKeyException
                                               reason:[NSString stringWithFormat:@"Context saving error: %@\n%@\n%@", error.domain, error.description, error.userInfo]
                                             userInfo:error.userInfo];
            }

            if (completionBlock) {
                if (onMainThread && [NSThread isMainThread]) {
                    completionBlock();
                } else if (onMainThread) {
                    dispatch_async(dispatch_get_main_queue(), ^{
                        completionBlock();
                    });
                } else if ([NSThread isMainThread]) {
                    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul), ^{
                        completionBlock();
                    });
                } else {
                    completionBlock();
                }
            }
        }];
    }
}

Then on some stage I'm calling method of CoreDataManager to save master context:

- (void)saveMasterContext; {
    __block NSError *error;

    [self.mainContext performBlock:^{
        [self.mainContext save:&error];
        [self treatError:error];

        [self.masterContext performBlock:^{
            [self.masterContext save:&error];
            [self treatError:error];
        }];
    }];
}

I have two main classes, subclasses of NSManagedObject - PlayerView and Post. PlayerView has relation one to many to Post. PlayerView is saved and is ok. The Post is never saved and I get error:

CoreData: error: Mutating a managed object 0x17dadd80 (0x17daf930) after it has been removed from its context.

I think, that the problem is in contexts saving logic.

2
one question is does this ever get executed? else if ([NSThread isMainThread]) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul), ^{ completionBlock(); }); if the thread that gets generated is not the same thread that the PlayerView's callback is on, the context passed in will not be on the correct thread, if you reference it in place of // do something.mitrenegade

2 Answers

0
votes

First of all, the error you're experiencing usually happens when the context in which you created the new managed object goes away (released) before you had the chance to save it.

Secondly, the best way to make sure the context is saved in the appropriate thread is to use performBlock or performBlockAndWait instead of trying to figure out which thread the context belongs to. Here's a sample "save" function that saves the context safely:

+ (BOOL)save:(NSManagedObjectContext *)context {
    __block BOOL saved = NO;
    [context performBlockAndWait: {
        NSError *error;
        saved = [context save:&error];
        if (!saved) {
            NSLog("failed to save: %@", error);
        }
    }]
    return saved;
}

As for using nested private contexts (with main thread context as the parent), our team experienced some issues with that model (can't recall exactly what it was), but we decided to listen for NSManagedObjectContextDidSaveNotification and use mergeChangesFromContextDidSaveNotification to update contexts.

I hope this helps.

0
votes

A great tutorial by Bart Jacobs entitled: Core Data from Scratch: Concurrency describes two approaches in detail, the more elegant solution involves parent/child managed object contexts, including how to properly save context.