I've read a number of posts here about NSManagedObjectContext and multi-threaded applications. I've also gone over the CoreDataBooks example to understand how separate threads require their own NSManagedObjectContext, and how a save operation gets merged with the main NSManagedObjectContext. I found the example to be good, but also too application specific. I'm trying to generalize this, and wonder if my approach is sound.
My approach is to have a generic function for fetching the NSManagedObjectContext for the current thread. The function returns the NSManagedObjectContext for the main thread, but will create a new one (or fetch it from a cache) if called from within a different thread. That goes as follows:
+(NSManagedObjectContext *)managedObjectContext {
MyAppDelegate *delegate = (MyAppDelegate *)[[UIApplication sharedApplication] delegate];
NSManagedObjectContext *moc = delegate.managedObjectContext;
NSThread *thread = [NSThread currentThread];
if ([thread isMainThread]) {
return moc;
}
// a key to cache the context for the given thread
NSString *threadKey = [NSString stringWithFormat:@"%p", thread];
// delegate.managedObjectContexts is a mutable dictionary in the app delegate
NSMutableDictionary *managedObjectContexts = delegate.managedObjectContexts;
if ( [managedObjectContexts objectForKey:threadKey] == nil ) {
// create a context for this thread
NSManagedObjectContext *threadContext = [[[NSManagedObjectContext alloc] init] autorelease];
[threadContext setPersistentStoreCoordinator:[moc persistentStoreCoordinator]];
// cache the context for this thread
[managedObjectContexts setObject:threadContext forKey:threadKey];
}
return [managedObjectContexts objectForKey:threadKey];
}
Save operations are simple if called from the main thread. Save operations called from other threads require merging within the main thread. For that I have a generic commit
function:
+(void)commit {
// get the moc for this thread
NSManagedObjectContext *moc = [self managedObjectContext];
NSThread *thread = [NSThread currentThread];
if ([thread isMainThread] == NO) {
// only observe notifications other than the main thread
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(contextDidSave:)
name:NSManagedObjectContextDidSaveNotification
object:moc];
}
NSError *error;
if (![moc save:&error]) {
// fail
}
if ([thread isMainThread] == NO) {
[[NSNotificationCenter defaultCenter] removeObserver:self
name:NSManagedObjectContextDidSaveNotification
object:moc];
}
}
In the contextDidSave:
function we perform the merge, if called by the notification in commit
.
+(void)contextDidSave:(NSNotification*)saveNotification {
MyAppDelegate *delegate = (MyAppDelegate *)[[UIApplication sharedApplication] delegate];
NSManagedObjectContext *moc = delegate.managedObjectContext;
[moc performSelectorOnMainThread:@selector(mergeChangesFromContextDidSaveNotification:)
withObject:saveNotification
waitUntilDone:YES];
}
Finally, we clean-up the cache of NSManagedObjectContext with this:
+(void)initialize {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(threadExit)
name:NSThreadWillExitNotification
object:nil];
}
+(void)threadExit {
MyAppDelegate *delegate = (MyAppDelegate *)[[UIApplication sharedApplication] delegate];
NSString *threadKey = [NSString stringWithFormat:@"%p", [NSThread currentThread]];
NSMutableDictionary *managedObjectContexts = delegate.managedObjectContexts;
[managedObjectContexts removeObjectForKey:threadKey];
}
This compiles and seems to work, but I know threading problems can be tricky due to race conditions. Does anybody see a problem with this approach?
Also, I'm using this from within the context of an asynchronous request (using ASIHTTPRequest), which fetches some data from a server and updates and inserts the store on the iPhone. It seems NSThreadWillExitNotification doesn't fire after the request completes, and the same thread is then used for subsequent requests. This means the same NSManagedObjectContext is used for separate requests on the same thread. Is this a problem?