22

I'm using Grand Central Dispatch (GCD) in my application to do some heavy lifting. The application is using Core-Data for data storage purposes. Here's my scenario (along with relevant question):

dispatch_queue_t main_queue = dispatch_get_main_queue();
dispatch_queue_t request_queue = dispatch_queue_create("com.app.request", NULL);

dispatch_async(request_queue, ^{
    MyNSManagedObject *mObject = [self.fetchedResultsController objectAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];

    // … 
    // <heavy lifting>
    // … 

    // … 
    // <update mObject>
    // … 

    [self saveManagedObjectContext];
});     

As a result of [self saveManagedObjectContext], fetchResultsController delegate methods are called automatically. Consequently, the UI updation logic kicks.

Now my question is, do I need to use main_queue for -saveManagedObjectContext? Should I perform all operations on my NSManagedObject in main_queue? Some of the operations that update the NSManagedObject might take 2-3 seconds. Please advise.

Mustafa
  • 20,504
  • 42
  • 146
  • 209

3 Answers3

60

There is a golden rule when it comes to Core Data - one Managed Object Context per thread. Managed object contexts are not thread safe so if you are doing work in a background task you either use the main thread to avoid threading conflicts with UI operations, or you create a new context to do the work in. If the work is going to take a few seconds then you should do the latter to stop your UI from locking up.

To do this you create a new context and give it the same persistent store as your main context:

NSManagedObjectContext *backgroundContext = [[[NSManagedObjectContext alloc] init] autorelease];
[backgroundContext setPersistentStoreCoordinator:[mainContext persistentStoreCoordinator]];

Do whatever operations you need to do, then when you save that new context you need to handle the save notification and merge the changes into your main context with the mergeChangesFromContextDidSaveNotification: message. The code should look something like this:

/* Save notification handler for the background context */
- (void)backgroundContextDidSave:(NSNotification *)notification {
    /* Make sure we're on the main thread when updating the main context */
    if (![NSThread isMainThread]) {
        [self performSelectorOnMainThread:@selector(backgroundContextDidSave:)
                               withObject:notification
                            waitUntilDone:NO];
        return;
    }

    /* merge in the changes to the main context */
    [self.managedObjectContext mergeChangesFromContextDidSaveNotification:notification];
}

/* ... */

/* Save the background context and handle the save notification */
[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(backgroundContextDidSave:)
                                             name:NSManagedObjectContextDidSaveNotification
                                           object:backgroundContext];

[backgroundContext save:NULL];

[[NSNotificationCenter defaultCenter] removeObserver:self
                                                name:NSManagedObjectContextDidSaveNotification
                                              object:syncContext];

Handling the save notifcation and merging is important otherwise your main UI/context won't see the changes you made. By merging, your main fetchResultsController etc. will get change events and update your UI as you would expect.

Another important thing to note is that NSManagedObject instances can only be used in the context that they were fetched from. If your operation needs a reference to an object then you have to pass the object's objectID to the operation and re-fetch an NSManagedObject instance from the new context using existingObjectWithID:. So something like:

/* This can only be used in operations on the main context */
MyNSManagedObject *objectInMainContext =
    [self.fetchedResultsController objectAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];

/* This can now be used in your background context */
MyNSManagedObject *objectInBackgroundContext =
    (MyNSManagedObject *) [backgroundContext existingObjectWithID:[objectInMainContext objectID]];
Mike Weller
  • 45,401
  • 15
  • 131
  • 151
  • 1
    So, what your saying is that in my case, instead of using fetchedResultsController, i should create a new managed object context (backgroundManagedObjectContext), fetch the required managed object, perform required operations, update the managed object, save this managed object context (backgroundManagedObjectContext), and then merge changes to reflect in the main managed object context? This will make my life pretty darn miserable. – Mustafa Nov 24 '10 at 08:54
  • You can still use fetched results controller to display and update stuff in your UI. The code I have shown you is all that is required to perform whatever operations you need in a separate context, nothing else should change. – Mike Weller Nov 24 '10 at 09:58
  • There are ways around this golden rule: https://github.com/adam-roth/coredata-threadsafe. – aroth Jun 14 '12 at 07:21
  • 1
    With the line -removeObserver: did you mean 'backgroundContext' instead of 'syncContext' for the object: parameter? My apologies if that's a daft question, I'm really struggling with threaded core data manipulation! – Todd Dec 11 '12 at 22:27
  • @aroth, on your github repository you state, "Update every class that you have that derives from NSManagedObject so that it derives from IAThreadSafeManagedObject instead. To accomplish this, do: #import (at)interface MyManagedObjectSubclass : IAThreadSafeManagedObject" Does this mean the Xcode-generated classes must be modified? – Victor Engel Jun 30 '13 at 16:57
  • @aroth, I think I've run into a problem. How can I contact you to troubleshoot? I'm getting a crash at IAThreadSafeManagedObject.m line 73 `[self setPrimitiveValue:obj forKey:propertyName];` after several thousand successful transactions. – Victor Engel Jul 03 '13 at 22:04
  • @aroth, never mind. I've opted for an alternative. But, in case you wish to pursue this, the problem came when I added dozens of views as subviews to a new view, then quickly moved them to a different view and deleted the one they originally were made subviews of. The problem occurred because threads adding the relationship were still taking place after the view had been deleted, so it was trying to set up a relationship with a nonexistent object. Using the isDeleted method did not prevent this problem. – Victor Engel Jul 06 '13 at 00:03
17

As you probably know or have noticed you must perform UI operations on the main thread. As you mention it is when you save the UI update takes place. You can solve this by nesting a call to dispatch_sync on the main thread.

dispatch_queue_t main_queue = dispatch_get_main_queue();
dispatch_queue_t request_queue = dispatch_queue_create("com.app.request", NULL);

__block __typeof__(self) blockSelf = self;

dispatch_async(request_queue, ^{
    MyNSManagedObject *mObject = [blockSelf.fetchedResultsController objectAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];

    // update and heavy lifting...

    dispatch_sync(main_queue, ^{
      [blockSelf saveManagedObjectContext];
    });
});     

The use of blockSelf is to avoid creating accidentally reference cycles. (Practical blocks)

Robert Höglund
  • 10,010
  • 13
  • 53
  • 70
0

Since Core Data requires one Managed Object Context per thread, a possible solution would be to track a context per Thread in a global manager, then track save notifications and propagate to all Threads:

Assuming:

@property (nonatomic, strong) NSDictionary* threadsDictionary;

Here is how to get the managed object (per thread):

- (NSManagedObjectContext *) managedObjectContextForThread {

// Per thread, give one back
NSString* threadName = [NSString stringWithFormat:@"%d",[NSThread currentThread].hash];

NSManagedObjectContext * existingContext = [self.threadsDictionary objectForKey:threadName];
if (existingContext==nil){
    existingContext = [[NSManagedObjectContext alloc] init];
    [existingContext setPersistentStoreCoordinator: [self persistentStoreCoordinator]];
    [self.threadsDictionary setValue:existingContext forKey:threadName];
}

return existingContext;

}

At some point in the init method of your global manager (I used a singleton):

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(backgroundContextDidSave:)                                                    name:NSManagedObjectContextDidSaveNotification                                                   object:nil];

Then to receive save notifications and propagate to all other managed context objects:

- (void)backgroundContextDidSave:(NSNotification *)notification {
    /* Make sure we're on the main thread when updating the main context */
    if (![NSThread isMainThread]) {
        [self performSelectorOnMainThread:@selector(backgroundContextDidSave:)
                               withObject:notification
                            waitUntilDone:NO];
        return;
    }

    /* merge in the changes to the main context */
    for (NSManagedObjectContext* context in [self.threadsDictionary allValues]){
            [context mergeChangesFromContextDidSaveNotification:notification];
    }
}

(some other methods were removed for clarity)

Resh32
  • 6,500
  • 3
  • 32
  • 40