0
votes

I setup a background thread with the Parent/Child model. Essentially the context save is failing.

Here is my setup. In the AppDelegate i've setup the _managedObjectContext with the NSMainQueueConcurrencyType:

- (NSManagedObjectContext *)managedObjectContext
{
    if (_managedObjectContext != nil) {
        return _managedObjectContext;
    }

    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
    if (coordinator != nil) {

        _managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];//[[NSManagedObjectContext alloc] init];
        [_managedObjectContext setPersistentStoreCoordinator:coordinator];
    }
    return _managedObjectContext;
}

In my data loading class I setup the parent/child mocs here to perform the work on the background thread:

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_async(queue, ^{

    NSManagedObjectContext *mainMOC = self.managedObjectContext;
    NSManagedObjectContext *moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];


    [moc setParentContext:mainMOC];
    [moc setUndoManager:nil];

When the json data has completed I attempt to peform a save operation with the following macro:

#define SAVE_MOC    { NSError *error; \
if (![moc save:&error]) { NSLog(@"Sub MOC Error"); } \
[mainMOC performBlock:^{  NSError *e = nil;  if (![mainMOC save:&e]) {    

NSLog(@"Main MOC Error %@",error.localizedDescription);}}];}

Also when i've completed the data load I jump back on the main thread like this:

dispatch_async(dispatch_get_main_queue(), ^{
    NSLog(@"<---- complete CS sub moc! ---->");
    //this fires ok

});

So, from my SAVE_MOC macro i just get a simple error: Main MOC Error (null)

Let me know if I can provide more info. I'm very new to multi-threading and trying to get a better handle on this approach.

Thanks, Josh

1
You log the wrong error, the parameter should be e, not error. Perform block is asynchronous, so can you guarantee execution order? Also confinement is deprecated so you should use perform blocks for everything except main context on the main thread. - Wain

1 Answers

2
votes

In my data loading class I setup the parent/child mocs here to perform the work on the background thread:

dispatch_queue_t queue =
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
    NSManagedObjectContext *mainMOC = self.managedObjectContext;
    NSManagedObjectContext *moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];

You should not do that. Do this instead.

NSManagedObjectContext *mainMOC = self.managedObjectContext;
NSManagedObjectContext *moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];

Make sure you access the MOC in a performBlock. For example,

[moc performBlock:^{
    // Anything at all involving this MOC or any of its objects
}];

When the json data has completed I attempt to peform a save operation with the following macro:

Consider saving with something like this. Your completion block will be called when the save has finished.

- (void)saveMOC:(NSManagedObjectContext*)moc
     completion:(void(^)(NSError *error))completion {
    [moc performBlock:^{
        NSError *error = nil;
        if ([moc save:&error]) {
            if (moc.parentContext) {
                return [self saveMOC:moc.parentContext completion:completion];
            }
        }
        if (completion) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completion(error);
            });
        }
    }];
}

[self saveMOC:moc completion:^(NSError *error) {
    // Completion handler is called from main-thread, after save has finished
    if (error) {
        // Handle error
    } else {
    }
}];

EDIT

This code will crash if moc.parentContext is main concurrency type. – Mundi

There is no inherent reason that the code I posted should cause a crash with a parent MOC of NSMainQueueConcurrencyType. It has supported being a parent context ever since parent/child was added to Core Data.

Maybe I was missing a typo, so I copy/paste saveMOC:completion: straight from this answer, and wrote the following test helper.

- (void)testWithChildConcurrencyType:(NSManagedObjectContextConcurrencyType)childConcurrencyType
               parentConcurrencyType:(NSManagedObjectContextConcurrencyType)parentConcurrencyType {
    NSAttributeDescription *attr = [[NSAttributeDescription alloc] init];
    attr.name = @"attribute";
    attr.attributeType = NSStringAttributeType;
    NSEntityDescription *entity = [[NSEntityDescription alloc] init];
    entity.name = @"Entity";
    entity.properties = @[attr];
    NSManagedObjectModel *model = [[NSManagedObjectModel alloc] init];
    model.entities = @[entity];

    NSPersistentStoreCoordinator *psc = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model];
    [psc addPersistentStoreWithType:NSInMemoryStoreType configuration:nil URL:nil options:nil error:NULL];
    NSManagedObjectContext *parent = [[NSManagedObjectContext alloc] initWithConcurrencyType:parentConcurrencyType];
    parent.persistentStoreCoordinator = psc;

    NSManagedObjectContext *child = [[NSManagedObjectContext alloc] initWithConcurrencyType:childConcurrencyType];
    child.parentContext = parent;

    NSManagedObject *obj = [NSEntityDescription insertNewObjectForEntityForName:@"Entity" inManagedObjectContext:child];
    [obj setValue:@"value" forKey:@"attribute"];

    XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"save from %@ to %@ finished", concurrencyTypeString(childConcurrencyType), concurrencyTypeString(parentConcurrencyType)]];
    [self saveMOC:child completion:^(NSError *error) {
        // Verify data saved all the way to the PSC
        NSManagedObjectContext *localMoc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
        localMoc.persistentStoreCoordinator = psc;
        NSFetchRequest *fr = [NSFetchRequest fetchRequestWithEntityName:@"Entity"];
        XCTAssertEqualObjects(@"value", [[[localMoc executeFetchRequest:fr error:NULL] firstObject] valueForKey:@"attribute"]);
        [expectation fulfill];
    }];
    [self waitForExpectationsWithTimeout:10 handler:nil];
}

And then, I wrote a test for each possible parent/child relationship.

- (void)testThatDoingRecursiveSaveFromPrivateToPrivateWorks {
    [self testWithChildConcurrencyType:NSPrivateQueueConcurrencyType
                 parentConcurrencyType:NSPrivateQueueConcurrencyType];
}
- (void)testThatDoingRecursiveSaveFromPrivateToMainWorks {
    [self testWithChildConcurrencyType:NSPrivateQueueConcurrencyType
                 parentConcurrencyType:NSMainQueueConcurrencyType];
}
- (void)testThatDoingRecursiveSaveFromMainToPrivateWorks {
    [self testWithChildConcurrencyType:NSMainQueueConcurrencyType
                 parentConcurrencyType:NSPrivateQueueConcurrencyType];
}
- (void)testThatDoingRecursiveSaveFromMainToMainWorks {
    [self testWithChildConcurrencyType:NSMainQueueConcurrencyType
                 parentConcurrencyType:NSMainQueueConcurrencyType];
}

So, what am I missing?

As I write this, I am reminded of a 360iDev presentation where the presenter said that you can't call performBlock on a NSMainQueueConcurrencyType context. At the time, I thought he just misspoke, meaning confinement, but maybe there is some confusion in the community about this.

You can't call performBlock on a NSConfinementConcurrencyType MOC, but performBlock is fully supported for NSMainQueueConcurrencyType.