Core Data saving objects in background issue

14,096

Solution 1

in your case because your writing to the background moc the notification for mergeChangesFromContextDidSaveNotification will come in on the background moc, not the foreground moc.

so you'll need to register for notifications on the background thread coming to the background moc object.

when you receive that call you can send a message to the main thread moc to mergeChangesFromContextDidSaveNotification.

andrew

update: here's a sample that should work

    //register for this on the background thread
    NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
    [nc addObserver:self selector:@selector(mergeChanges:) name:NSManagedObjectContextDidSaveNotification object:backgroundMOC];

- (void)mergeChanges:(NSNotification *)notification {
    NSManagedObjectContext *mainThreadMOC = [singleton managedObjectContext];

    //this tells the main thread moc to run on the main thread, and merge in the changes there
    [mainThreadMOC performSelectorOnMainThread:@selector(mergeChangesFromContextDidSaveNotification:) withObject:notification waitUntilDone:YES];
}

Solution 2

I'm going to throw this out there. Stop following the best practices for concurrency listed in the Core Data Programming Guide. Apple has not updated it since adding nested contexts which are MUCH easier to use. This video goes into full detail: https://developer.apple.com/videos/wwdc/2012/?id=214

Setup your primary context to use your main thread (appropriate for handling UI):

NSManagedObjectContext * context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[context setPersistentStoreCoordinator:yourPSC];

For any object you create that may be doing concurrent operations, create a private queue context to use

NSManagedObjectContext * backgroundContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[backgroundContext setParentContext:context];
//Use backgroundContext to insert/update...
//Then just save the context, it will automatically sync to your primary context
[backgroundContext save:nil];

The QueueConcurrencyType refers to the queue the context will do it's fetch (save and fetch request) operations on. The NSMainQueueConcurrencyType context does all it's work on the main queue, which makes it appropriate for UI interaction. A NSPrivateQueueConcurrencyType does it on it's own private queue. So when you call save on the backgroundContext, it merges it's private data calling the parentContext using performBlock as appropriate automatically. You don't want to call performBlock on the private queue context in case it happens to be on the main thread which will cause a deadlock.

If you want to get really fancy, You can create a primary context as a private queue concurrency type (which is appropriate for background saving) with a main queue context for just your UI and then child contexts of your main queue context for background operations (like imports).

Solution 3

I see you've worked out an answer that works for you. But I have been having some similar issues and wanted to share my experience and see if it is at all helpful to you or others looking at this situation.

Multi-threaded Core Data stuff is always a little confusing to read, so please excuse me if I misread your code. But it appears that there could be a simpler answer for you.

The core issue you had in the first attempt is that you saved off managed object IDs (supposedly the object identifiers that can be passed between threads) to a global variable for use on the main thread. You did this on a background thread. The problem was that you did this BEFORE saving to the background thread's managed object context. Object IDs are not safe to pass to another thread/context pair prior to a save. They can change when you save. See the warning in the documentation of objectID: NSManagedObject reference

You fixed this by notifying your background thread of the save, and inside that thread, grabbing the now-safe-to-use-because-the-context-has-been-saved object IDs from the notification object. These were passed to the main thread, and the actual changes were also merged into the main thread with the call to mergeChangesFromContextDidSaveNotification. Here's where you might save a step or two.

You are registering to hear the NSManagedObjectContextDidSaveNotification on the background thread. You can register to hear that same notification on the main thread instead. And in that notification you will have the same object IDs that are safe to use on the main thread. The main thread MOC can be safely updated using mergeChangesFromContextDidSaveNotification and the passed notification object, since the method is designed to work this way: mergeChanges docs. Calling your completion block from either thread is now safe as long as you match the moc to the thread the completion block is called on.

So you can do all your main thread updating stuff on the main thread, cleanly separating the threads and avoiding having to pack and repack the updated stuff or doing a double save of the same changes to the persistent store.

To be clear - the Merge that happens is on the managed object contextand its in-memory state - the moc on the main thread is updated to match the one on the background thread, but a new save isn't necessary since you ALREADY saved these changes to the store on the background thread. You have thread safe access to any of those updated objects in the notification object, just as you did when you used it on the background thread.

I hope your solution is working for you and you don't have to re-factor - but wanted to add my thoughts for others who might see this. Please let me know if I've misinterpreted your code and I'll amend.

Share:
14,096

Related videos on Youtube

miken.mkndev
Author by

miken.mkndev

Updated on October 25, 2022

Comments

  • miken.mkndev
    miken.mkndev over 1 year

    What I'm trying todo in a nutshell is I am using a background queue to save JSON objects pulled from a web service to the Core Data Sqlite3 database. The saving takes place on a serialized background queue I've created via GCD, and saved to a secondary instance of NSManagedObjectContext that is created for that background queue. Once the save is complete I need to update the instance of NSManagedObjectContext that is on the main thread with the newly created/updated objects. The problem I am having though is the instance of NSManagedObjectContext on the main thread is not able to find the objects that were saved on the background context. Below is a list of actions I'm taking with code samples. Any thoughts on what I'm doing wrong?

    • Create a background queue via GCD, run all pre-processing logic and then save the background context on that thread:

    .

    // process in the background queue
    dispatch_async(backgroundQueue, ^(void){
    
        if (savedObjectIDs.count > 0) {
            [savedObjectIDs removeAllObjects];
        }
        if (savedObjectClass) {
            savedObjectClass = nil;
        }
    
        // set the thead name
        NSThread *currentThread = [NSThread currentThread];
        [currentThread setName:VS_CORE_DATA_MANAGER_BACKGROUND_THREAD_NAME];
    
        // if there is not already a background context, then create one
        if (!_backgroundQueueManagedObjectContext) {
            NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
            if (coordinator != nil) {
                _backgroundQueueManagedObjectContext = [[NSManagedObjectContext alloc] init];
                [_backgroundQueueManagedObjectContext setPersistentStoreCoordinator:coordinator];
            }
        }
    
        // save the JSON dictionary starting at the upper most level of the key path, and return all created/updated objects in an array
        NSArray *objectIds = [self saveJSON:jsonDict objectMapping:objectMapping class:managedObjectClass managedObjectContext:_backgroundQueueManagedObjectContext level:0];
    
        // save the object IDs and the completion block to global variables so we can access them after the save
        if (objectIds) {
            [savedObjectIDs addObjectsFromArray:objectIds];
        }
        if (completion) {
            saveCompletionBlock = completion;
        }
        if (managedObjectClass) {
            savedObjectClass = managedObjectClass;
        }
    
        // save all changes object context
        [self saveManagedObjectContext];
    });
    
    • The "saveManagedObjectContext" method basically looks at which thread is running and saves the appropriate context. I have verified that this method is working correctly so I will not place the code here.

    • All of this code resides in a singleton, and in the singleton's init method I am adding a listener for the "NSManagedObjectContextDidSaveNotification" and it calls the mergeChangesFromContextDidSaveNotification: method

    .

    // merge changes from the context did save notification to the main context
    - (void)mergeChangesFromContextDidSaveNotification:(NSNotification *)notification
    {
        NSThread *currentThread = [NSThread currentThread];
    
        if ([currentThread.name isEqual:VS_CORE_DATA_MANAGER_BACKGROUND_THREAD_NAME]) {
    
            // merge changes to the primary context, and wait for the action to complete on the main thread
            [_managedObjectContext performSelectorOnMainThread:@selector(mergeChangesFromContextDidSaveNotification:) withObject:notification waitUntilDone:YES];
    
            // on the main thread fetch all new data and call the completion block
            dispatch_async(dispatch_get_main_queue(), ^{
    
                // get objects from the database
                NSMutableArray *objects = [[NSMutableArray alloc] init];
                for (id objectID in savedObjectIDs) {
                    NSError *error;
                    id object = [_managedObjectContext existingObjectWithID:objectID error:&error];
                    if (error) {
                        [self logError:error];
                    } else if (object) {
                        [objects addObject:object];
                    }
                }
    
                // remove all saved object IDs from the array
                [savedObjectIDs removeAllObjects];
                savedObjectClass = nil;
    
                // call the completion block
                //completion(objects);
                saveCompletionBlock(objects);
    
                // clear the saved completion block
                saveCompletionBlock = nil;
            });
        }
    }
    

    As you can see in the method above I am calling the "mergeChangesFromContextDidSaveNotification:" on the main thread, and I have set the action to wait until done. According to the apple documentation the background thread should wait until that action is complete before it continues with the rest of the code below that call. As I mentioned above once I run this code everything seems to work, but when I try to print out the fetched objects to the console I don't get anything back. It seems that the merge is not in fact taking place, or possibly not finishing before the rest of my code runs. Is there another notification that I should be listening for to ensure that the merge has completed? Or do I need to save the main object context after the merge, but before the fecth?

    Also, I apologize for the bad code formatting, but it seems that SO's code tags don't like method definitions.

    Thanks guys!

    UPDATE:

    I've made the changes that were recommended below, but still having the same problem. Below is the updated code I have.

    This is the code that invokes the background thread saving processes

    // process in the background queue
    dispatch_async(backgroundQueue, ^(void){
    
        if (savedObjectIDs.count > 0) {
            [savedObjectIDs removeAllObjects];
        }
        if (savedObjectClass) {
            savedObjectClass = nil;
        }
    
        // set the thead name
        NSThread *currentThread = [NSThread currentThread];
        [currentThread setName:VS_CORE_DATA_MANAGER_BACKGROUND_THREAD_NAME];
    
        // if there is not already a background context, then create one
        if (!_backgroundQueueManagedObjectContext) {
            NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
            if (coordinator != nil) {
                _backgroundQueueManagedObjectContext = [[NSManagedObjectContext alloc] init];
                [_backgroundQueueManagedObjectContext setPersistentStoreCoordinator:coordinator];
            }
        }
    
        // save the JSON dictionary starting at the upper most level of the key path
        NSArray *objectIds = [self saveJSON:jsonDict objectMapping:objectMapping class:managedObjectClass managedObjectContext:_backgroundQueueManagedObjectContext level:0];
    
        // save the object IDs and the completion block to global variables so we can access them after the save
        if (objectIds) {
            [savedObjectIDs addObjectsFromArray:objectIds];
        }
        if (completion) {
            saveCompletionBlock = completion;
        }
        if (managedObjectClass) {
            savedObjectClass = managedObjectClass;
        }
    
        // listen for the merge changes from context did save notification
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(mergeChangesFromBackground:) name:NSManagedObjectContextDidSaveNotification object:_backgroundQueueManagedObjectContext];
    
        // save all changes object context
        [self saveManagedObjectContext];
    });
    

    This is the code that is called with by the NSManagedObjectContextDidSaveNotification notification

        // merge changes from the context did save notification to the main context
    - (void)mergeChangesFromBackground:(NSNotification *)notification
    {
        // kill the listener
        [[NSNotificationCenter defaultCenter] removeObserver:self name:NSManagedObjectContextDidSaveNotification object:_backgroundQueueManagedObjectContext];
    
        NSThread *currentThread = [NSThread currentThread];
    
        // merge changes to the primary context, and wait for the action to complete on the main thread
        [[self managedObjectContext] performSelectorOnMainThread:@selector(mergeChangesFromContextDidSaveNotification:) withObject:notification waitUntilDone:YES];
    
        // dispatch the completion block
        dispatch_async(dispatch_get_main_queue(), ^{
    
            // get objects from the database
            NSMutableArray *objects = [[NSMutableArray alloc] init];
            for (id objectID in savedObjectIDs) {
                NSError *error;
                id object = [[self managedObjectContext] existingObjectWithID:objectID error:&error];
                if (error) {
                    [self logError:error];
                } else if (object) {
                    [objects addObject:object];
                }
            }
    
            // remove all saved object IDs from the array
            [savedObjectIDs removeAllObjects];
            savedObjectClass = nil;
    
            // call the completion block
            //completion(objects);
            saveCompletionBlock(objects);
    
            // clear the saved completion block
            saveCompletionBlock = nil;
        });
    }
    

    UPDATE:

    So I found the solution. Turns out that the way I was saving out the object IDs on the background thread and then trying to use them on the main thread to re-fetch them wasn't working out. So I ended up pulling the inserted/updated objects from the userInfo dictionary that is sent with the NSManagedObjectContextDidSaveNotification notification. Below is my updated code that is now working.

    As before this code starts the pre-prossesing and saving logic

    // process in the background queue
    dispatch_async(backgroundQueue, ^(void){
    
        // set the thead name
        NSThread *currentThread = [NSThread currentThread];
        [currentThread setName:VS_CORE_DATA_MANAGER_BACKGROUND_THREAD_NAME];
    
        [self logMessage:[NSString stringWithFormat:@"(%@) saveJSONObjects:objectMapping:class:completion:", [managedObjectClass description]]];
    
        // if there is not already a background context, then create one
        if (!_backgroundQueueManagedObjectContext) {
            NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
            if (coordinator != nil) {
                _backgroundQueueManagedObjectContext = [[NSManagedObjectContext alloc] init];
                [_backgroundQueueManagedObjectContext setPersistentStoreCoordinator:coordinator];
            }
        }
    
        // save the JSON dictionary starting at the upper most level of the key path
        [self saveJSON:jsonDict objectMapping:objectMapping class:managedObjectClass managedObjectContext:_backgroundQueueManagedObjectContext level:0];
    
        // save the object IDs and the completion block to global variables so we can access them after the save
        if (completion) {
            saveCompletionBlock = completion;
        }
    
        // listen for the merge changes from context did save notification
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(mergeChangesFromBackground:) name:NSManagedObjectContextDidSaveNotification object:_backgroundQueueManagedObjectContext];
    
        // save all changes object context
        [self saveManagedObjectContext];
    });
    

    This is the modified method that handles the NSManagedObjectContextDidSaveNotification

    - (void)mergeChangesFromBackground:(NSNotification *)notification
    {
        // kill the listener
        [[NSNotificationCenter defaultCenter] removeObserver:self name:NSManagedObjectContextDidSaveNotification object:_backgroundQueueManagedObjectContext];
    
        // merge changes to the primary context, and wait for the action to complete on the main thread
        [[self managedObjectContext] performSelectorOnMainThread:@selector(mergeChangesFromContextDidSaveNotification:) withObject:notification waitUntilDone:YES];
    
        // dispatch the completion block
        dispatch_async(dispatch_get_main_queue(), ^{
    
            // pull the objects that were saved from the notification so we can get them on the main thread MOC
            NSDictionary *userInfo = [notification userInfo];
            NSMutableArray *modifiedObjects = [[NSMutableArray alloc] init];
            NSSet *insertedObject = (NSSet *)[userInfo objectForKey:@"inserted"];
            NSSet *updatedObject = (NSSet *)[userInfo objectForKey:@"updated"];
    
            if (insertedObject && insertedObject.count > 0) {
                [modifiedObjects addObjectsFromArray:[insertedObject allObjects]];
            }
            if (updatedObject && updatedObject.count > 0) {
                [modifiedObjects addObjectsFromArray:[updatedObject allObjects]];
            }
    
            NSMutableArray *objects = [[NSMutableArray alloc] init];
    
            // iterate through the updated objects and find them in the main thread MOC
            for (NSManagedObject *object in modifiedObjects) {
                NSError *error;
                NSManagedObject *obj = [[self managedObjectContext] existingObjectWithID:object.objectID error:&error];
                if (error) {
                    [self logError:error];
                }
                if (obj) {
                    [objects addObject:obj];
                }
            }
    
            modifiedObjects = nil;
    
            // call the completion block
            saveCompletionBlock(objects);
    
            // clear the saved completion block
            saveCompletionBlock = nil;
        });
    }
    
  • miken.mkndev
    miken.mkndev over 11 years
    Ok, thanks for the quick reply. I've tried to add the listener (posted below) right above the saveManagedObjectContext method call, but I'm still getting the same result. Am I adding the observer correctly or is there another way to add an observer to a specific thread? // listen for the merge changes from context did save notification [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(mergeChangesFromContextDidSaveNotificatio‌​n:) name:NSManagedObjectContextDidSaveNotification object:_backgroundQueueManagedObjectContext];
  • miken.mkndev
    miken.mkndev over 11 years
    Hmm...this is a crazy issue. I've made the updates you suggested, but still not getting the new objects that were saved to the background MOC from the main thread MOC. I've even updated all my main thread MOC calls to use the factory method to make sure it's instantiated and everything, but still doesn't find the objects that were saved. I've also printed out the savedObjectIDs array and it is getting the IDs correctly. Can you see anything else that may be incorrect? Thanks again for the help! Also, I've updated my original post to have the updated code.
  • andrew lattis
    andrew lattis over 11 years
    have you tried adding some nslog statements to make sure the methods are getting called after you save? in particular is your mergeChangesFromBackground method being called?
  • miken.mkndev
    miken.mkndev over 11 years
    Yes, I did have some, but I removed them before posting my code. I figured that would just clutter things up on the forum. I actually found the solution to my problem, and am about to post that in a new answer on this forum. Thanks for all the help!
  • miken.mkndev
    miken.mkndev over 11 years
    SO wouldn't let me create a new answer, so I updated my original question with the updated code.
  • User
    User over 6 years
    I have a question, how to guarantee that the observer code is executed after the objects where saved in the main/view context? If I observe the save notification of the background context, the observer code could be executed before the background context was merged in the main context, right...?