Working with core data in a multi threaded environment you have to extremely careful
For example if were creating a news application, we want to be able to import news in the background and display updates to the user with no interruption of the GUI. The task can be divided into several stages:
- Download the xml feed with the latest news
- Parse the feed
- Insert the articles into core data
- Notify the UI and refresh the display.
We already have our core data API defined. A simple method to retrieve all the articles and a method to add a new article.
DataSource.h
@interface DataSource : NSObject { } -(NSArray*)getNewsFor:(Region*)region; -(void)addArticle:(Article*)article forRegion:(Region*)region; @end }
Now suppose we want to the web service to import the latest articles. We might define our implementation something like this:
WebService.h
@interface WebService : NSObject { id delegate; LocalDataSource * source; } @property(nonatomic, retain) id delegate; @property(nonatomic, retain) LocalDataSource * source; -(void)importNewsForRegion:(Region*)region; @end
Our implementation of importNewsForRegion would look something like this:
WebService.m
-(void)importNewsForRegion:(Region*)region { //Perform in background if called from main if ([NSThread currentThread] == [NSThread mainThread]) { [self performSelectorInBackground:@selector(importNewsForRegion:) withObject:region]; return; } //Setup pool NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; NSArray * articles = [XML parseArticleFeed:region.feedURL]; //Not safe 1. for (Article * article in articles) { [source addArticle:article forRegion:region]; //Not safe 2. } [delegate finishedImport]; //Not safe 3. [pool drain]; }
However this because importNewsForRegion is running in a separate thread this is unsafe and will likely cause core data to crash or corrupt our database.
- The region passed to to importNews has likely come from a core data context which was constructed on the main thread. Even though were not even altering the region directly with core data it’s not safe to even read the feedURL, because every operation can trigger faulting.
- When we instruct the source to add an article it will use the main threads context, this is fine when we are in the main thread, but in a background thread we need to specify our own context.
- Not related to core data, but when we notify the delegate it will likely perform UI updates which are not safe to perform in a background thread. we need to ensure the delegate only received notifications on the main thread.
To make our program thread safe we first need to make some small alterations to our API.
DataSource.h
@interface DataSource : NSObject { } -(NSArray*)getNewsFor:(Region*)region; -(NSArray*)getNewsFor:(Region*)region context:(NSManagedObjectContext*)context; -(void)addArticle:(Article*)article forRegion:(Region*)region; -(void)addArticle:(Article*)article forRegion:(Region*)region context:(NSManagedObjectContext*)context; @end }
In our data source we specify two more methods which allow us to provide a context for the method to use. All our queries should be updated to only use the provided context. The original methods that don’t specify a context should call the newly implemented method with the default context, e.g.
-(void)addArticle:(Article*)article forRegion:(Region*)region { [self addArticle:article forRegion:region context:mainThreadContext]; }
Only when we are using core data in a background thread do we need to use the newly added methods. So we can now update our WebService.m:
-(void)importNewsForRegion:(Region*)unsafeRegion { //Perform in background if called from main if ([NSThread currentThread] == [NSThread mainThread]) { [self performSelectorInBackground:@selector(importNewsForRegion:) withObject: unsafeRegion]; return; } //Setup pool NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; //Create a new context just for this thread NSManagedObjectContext * managedObjectContext = [[NSManagedObjectContext alloc] init]; [managedObjectContext setPersistentStoreCoordinator: [source persistentStoreCoordinator]]; //Create a new region which is safe to use in this thread Region * region = (Region*)[managedObjectContext existingObjectWithID:[unsafeRegion objectID] error:nil]; NSArray * articles = [XML parseArticleFeed:region.feedURL]; for (Article * article in articles) { //Ensure we provide the context for the source to use [source addArticle:article forRegion:region context:managedObjectContext]; } //Save changes and close the new context [managedObjectContext save:&error]; [managedObjectContext release]; [delegate performSelectorOnMainThread:@selector(finishedImport) withObject:nil waitUntilDone:NO]; [pool drain]; }
As you can see we crate a new context using out shared persistentStoreCoordinator, we ensure that anytime we touch core data from this thread we are using this context. The also means that we have to create a new region which belongs to this context, and we do this with the existingObjectWithID method and using the [unsafeRegion objectID]. Finally we ensure that the delegate receives the notification on the main thread.
for more information about threading in core data see:
http://developer.apple.com/Mac/library/documentation/Cocoa/Conceptual/CoreData/Articles/cdMultiThreading.html