Generic approach to NSManagedObjectContext in multi-threaded application - iphone

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?

A year after posting this question I finally built a framework to generalize and simplify my working with Core Data. It goes beyond the original question, and adds a number of features to make Core Data interactions much easier. Details here: https://github.com/chriscdn/RHManagedObject

I found a solution after finally understanding the problem better. My solution doesn't directly address the question above, but does address the problem of why I had to deal with threads in the first place.
My application uses the ASIHTTPRequest library for asynchronous requests. I fetch some data from the server, and use the delegate requestFinished function to add/modify/delete my core-data objects. The requestFinished function was running in a different thread, and I assumed this was a natural side-effect of asynchronous requests.
After digging deeper I found that ASIHTTPRequest deliberately runs the request in a separate thread, but can be overridden in my subclass of ASIHTTPRequest:
+(NSThread *)threadForRequest:(ASIHTTPRequest *)request {
return [NSThread mainThread];
}
This small change puts requestFinished in the main thread, which has eliminated my need to care about threads in my application.

Related

How to ensure NSManagedObjectContext when opened asynchronously through UIManagedDocument

I have an application with different controllers that all operate on the same NSManagedObjectContext.
My approach was to initialize the NSManagedObjectContext in my AppDelegate and inject it into all the controllers.
I am initializing my NSManagedObjectContext by opening a UIManagedDocument like this:
UIManagedDocument* databaseDoc = [[UIManagedDocument alloc] initWithFileURL:url];
if (![[NSFileManager defaultManager] fileExistsAtPath:[databaseDoc.fileURL path]]) {
[databaseDoc saveToURL:databaseDoc.fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
myController.managedObjectContext = databaseDoc.managedObjectContext;
}];
} else if (databaseDoc.documentState == UIDocumentStateClosed) {
[databaseDoc openWithCompletionHandler:^(BOOL success) {
myController.managedObjectContext = databaseDoc.managedObjectContext;
}];
} else if (databaseDoc.documentState == UIDocumentStateNormal){
myController.managedObjectContext = databaseDoc.managedObjectContext;
}
Now my problem is, that opening the UIManagedDocument happens asynchronously and the NSManagedObjectContext is only available in the completion block.
How do I ensure that the controllers always have a valid NSManagedObjectContext to work with? Of course the problems happen at startup i.e. when a controller wants to use the NSManagedObjectContext in his "viewDidLoad" method, and the completion block in the AppDelegate has not yet run ...
One approach would probably be to "wait" in the AppDelegate until the UIDocument has opened, but as far as I gather this is not recommended ...
I would like to avoid to "pollute" my controllers with code that deals with the asynchronous nature of opening a NSManagedObjectContext... but maybe this is a naive wish?
In your appDelegate:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
MyWaitViewController* waitController = [[MyWaitViewController new] autorelease];
self.window.rootViewController = waitController;
// then somewheres else, when you get your context
[databaseDoc saveToURL:databaseDoc.fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
myContextController.managedObjectContext = databaseDoc.managedObjectContext;
self.window.rootViewController = myContextController;
// note that at this point when the viewDidLoad method will get called
// it will have his managedObjectContext and his view already available.
// you can change your rootController, or push another viewController into the
// stack. Depending on what u want from the GUI side
}];
return YES;
}
Note that you dispose the GUI logic into the MyWaitViewController + AppDelegate side. But you keep your "myContextController" away from that logic control, since he get called / created only when a context exist.
I was struggling with the same issue, and I came up with it by using NSNotificationCenter.
When initializing your NSManagedObjectContext in the success handler, add send a notification.
Then, add a listener to to the viewDidLoad of whatever your first ViewController is.
I used that listener to call a reloadData method. In a heavy app, this could be a problem, as the viewcontroller loads blank, and then reloads the data, but this is a lite one, and it's noticeable at all - the viewController loads instantaneously with the managedObjectContext.

merge ManagedobjectContext in multi threads

My app have main thread which run every ten second and read data from core data and second thread to fetch data from server and load into coredata. after reading some example and also apple document I am still not sure the way i merge is correct:
1. i create new managedObjectContext for each save in database and set it to (single PersistentStore in appp delegate) .
2. have this code when i initate the class for loading data in user view:
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc addObserver:self
selector:#selector(mergeChanges:)
name:NSManagedObjectContextDidSaveNotification
object:appDelegate.managedObjectContext];
-(void)mergeChanges:(NSNotification *)notification
{
NSManagedObjectContext *mainContext = [appDelegate managedObjectContext];
// Merge changes into the main context on the main thread
[mainContext performSelectorOnMainThread:#selector(mergeChangesFromContextDidSaveNotification:)
withObject:notification
waitUntilDone:YES];
}
3- read data from coredata from main context
I am wondering if this is correct.
thanks

Call delegate method from a thread

I've this bit of code:
- (IBAction)registerAction:(id)sender {
[NSThread detachNewThreadSelector:#selector(registerThread) toTarget:self withObject:nil];
}
- (void)registerThread {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
MyDelegate *delegate = (MyDelegate *)[[UIApplication sharedApplication] delegate];
NSInteger locationID = [delegate.api checkCoordinate:[NSString stringWithFormat:#"%f,%f",
location.coordinate.latitude, location.coordinate.longitude]];
NSNumber *status = [api registerWithUsername:usernameField.text
password:passwordField.text email:emailField.text andLocation:locationID];
[self performSelectorOnMainThread:#selector(registrationDoneWithStatus:) withObject:[NSNumber numberWithInt:1] waitUntilDone:NO];
[pool release];
}
it works nicely, but sometimes I get this error:
void _WebThreadLockFromAnyThread(bool), 0x6157e30: Obtaining the web lock from a thread other than the main thread or the web thread. UIKit should not be called from a secondary thread.
And it seems that only using the delegate I get this error, and I don't know how to resolve.
Thanks in advance :)
I've run into the same problem recently.
There may be some active views (eg. UITextField,UITextView). Try resignFirstResponder those views before accessing delegate
You fix the problem by very carefully thinking through your application's concurrency architecture and ensuring that you aren't exercising anything from a thread that should only be done on the main thread.
In this case, you are causing the UIKit to execute code from a secondary thread. If you were to set a breakpoint on _WebThreadLockFromAnyThread, you would know exactly where.
It is exceedingly atypical to use the app's delegate from a secondary thread in anything but the most extremely controlled circumstances.
tl;dr You can't make an app threaded by detaching a new thread against a random selector.

Searching using NSOperation

I am trying to make a searchbar to search for things I receive using NSURLConnection.
right now, if I search for something, that string is send away as an URL with an asynchronous request, which gives me data.
NSURLRequest *theRequest=[NSURLRequest requestWithURL:[NSURL URLWithString:urlString] cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:20.0];
[theConnection cancel];
[theConnection release];
theConnection=[[NSURLConnection alloc] initWithRequest:theRequest delegate:self];
That data is parsed and when it is successful I post a notification
-(void)connectionDidFinishLoading:(NSURLConnection *)connection
{
xmlParser = [[NSXMLParser alloc] data];
[xmlParser setDelegate:xmlGeocoder];
BOOL success = [xmlParser parse];
if(success == YES){
NSLog(#"No Errors");
[[NSNotificationCenter defaultCenter] postNotificationName:#"getArray" object:self];
}else{
NSLog(#"Error Error Error!!!");
[[NSNotificationCenter defaultCenter] postNotificationName:#"failToGetArray" object:self];
}
}
and my searchresultsTableView is reloaded.
self.array1 = [array2 copy];
[self.searchDisplayController.searchResultsTableView reloadData];
All these methods are depending on eachother, so B can't be executed, when A is still busy.
I am using NSNotificationCenter to tell them to execute those code.
But I want to try NSOperation and I have no idea HOW to implement that.
Do I have to put my search requests in an operation or every method I'm using?
Can someone give me a sample code to give me the idea how this should be done?
Thanks in advance...
NSOperation is very useful. To use it you extend NSOperation and override the "main" method.
In the main method you do your calculations/web request etc. So NSOperation is best for tasks you can wrap into a few simple steps, after each step you test if everything is good and either continue to the next step or cancel the operation. Once this is done you can simply instantiate your custom NSOperation and hand it off to a NSOperationQueue object and it will take care of the threading, starting, stopping cleaning up etc.
In the example below I have written a protocol to handle the completion of the task, I would advise you take this approach instead of using notification - unless you have multiple objects that needs to be notified instantly.
Make a new class that extends the NSOperation class:
//This object takes a "searchTerm" and waits to be "started".
#import <Foundation/Foundation.h>
#protocol ISSearchOperationDelegate
- (void) searchDataReady:(NSArray*) searchResult;
#end
#interface ISSearchOperation : NSOperation {
id <ISSearchOperationDelegate> delegate;
NSString *searchTerm;
}
#property(nonatomic, retain) NSString *searchTerm;
#property(nonatomic, assign) id delegate;
- (id) initWithSearchTerm:(NSString*) searchString;
#end
When an object extending NSOperation is added to an NSOperationQueue, the queue object
tries to call a "main" method on the NSOperation, you must therefore wrap your task in this method.
(notice that after each completed sub-task I test if it went well and "return" if not. The NSOperation class
has a property called isCancelled This property can be set by the NSOperationQueue, so you must also
test if that has been set during your completion of main. So to recap, you test from the inside of main if each step went as you wanted and you test if something on the outside has cancelled your task.):
- (id) initWithSearchTerm:(NSString*) searchString {
if (self = [super init]) {
[self setSearchTerm:searchString];
}
return self;
}
- (void) main {
[self performSelector:#selector(timeOut) withObject:nil afterDelay:4.0];
if ([self isCancelled]) return;
NSData *resultData = [self searchWebServiceForString:self.searchTerm];
if (resultData == nil) return;
if ([self isCancelled]) return;
NSArray *result = [self parseJSONResult:resultData];
if ([self isCancelled]) return;
if (result == nil) return;
[NSObject cancelPreviousPerformRequestsWithTarget:self];
[delegate performSelectorOnMainThread:#selector(searchDataReady:) withObject:result waitUntilDone:YES];
}
//I have not copied the implementation of all the methods I call during main, but I hope you understand that they are just "tasks" that each must be successfully completed before the next sub-task can be computed.
So first of I put a timeout test in there, then I get my data from the web service and then I parse it.
Ok to get all this going you need a queue.
So in the class you want to be the delegate for this operation you do this:
somewhere set up a queue:
NSOperationQueue *q = [[NSOperationQueue alloc] init];
[self setQueue:q];
[q release];
- (void) doSearch:(NSString*) searchString {
[queue cancelAllOperations];
ISSearchOperation *searchOperation = [[ISSearchOperation alloc] initWithSearchTerm:searchString];
[searchOperation setDelegate:self];
[queue addOperation:searchOperation]; //this makes the NSOperationQueue call the main method in the NSOperation
[searchOperation release];
}
//the delegate method called from inside the NSOperation
- (void) searchDataReady:(NSArray*) results {
//Data is here!
}
Some of the advantages with NSOperations is that from the caller point of view, we simply make an object, set a delegate, wait for the reply. But behind the scenes a series of threaded tasks that can be cancelled at any time is run, and in a manner that can handle if threaded stuff fails.
As you can see in the doSearch method it starts out by canceling any previous operations, I did this in an app where I would search a web service each time a user typed a letter in a word. That means that if the user searched for "hello world" - I would do a search for "h", then "he", then "hel", then hell", then "hello" etc.
I wanted to stop and clean up the "h" task as soon as the user typed the "e", because it was then obsolete.
I found out NSOperation was the only way that gave the responsiveness of threading and none of the mess that usually comes with spawning many threads on top of each other.
Hope you can use it to get started:)

How to synchronise two NSManagedObjectContext

I'm working on an ipad application that use coredata. It download information on a database that is on the web, and record them in coredata. The application is based on a split view. My problem was to make the download and the record of the data in background. Here is how I've done :
- I've create an NSOperation, that does the download and the record of the data.
- This NSOperation use a different NSManagedObjectContext than the context of the appDelegate, return by this function, that is in the appDelegate :
(NSManagedObjectContext*)newContextToMainStore {
NSPersistentStoreCoordinator *coord = nil;
coord = [self persistentStoreCoordinator];
NSManagedObjectContext *moc = [[NSManagedObjectContext alloc] init];
[moc setPersistentStoreCoordinator:coord];
return [moc autorelease];
}
- I've had an observer in the NSOperation, that will call this function in the appDelegate when I save the context, to modify the context of the delegate too :
- (void)mergeChangesFromContextSaveNotification:(NSNotification*)notification {
[[self managedObjectContext]mergeChangesFromContextDidSaveNotification:notification];
}
But I've a problem, the synchronisation doesn't work, because the data on the rootViewController (that is a UITableViewController), that have a NSManagedObjectContext initialised with the context of the appDelegate and use as datasource a NSFetchedResultsController, don't actualise automatically the informations, as it normaly must do.
So I ask you : What did I do wrong ? Is it the good way to use two different context and synchonise them ?
What you have here looks correct. You do want to make sure you implement the NSFetchedResultControllerDelegate methods in the rootViewController so the changes will appear in the UI.