Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Core Data and multithreading

I have read Marcus Zarra's chapter on multithreading in his Core Data book and have looked fairly closely at his sample code. But his code and others that I have found elsewhere seem to be focused in background processes that do not need to be aware of each other. These examples are good for importing a tree structure - but they do not address the importing of a more general (complex) structure, like a directed acyclic graph.

In my case, I am trying to parse a C++ class hierarchy and would like to use as many NSOperations as possible. I would like to create an NSManagedObject instance for each encountered class and I would like to merge different NSManagedObjectContexts whenever one is saved.

As an aside: I am able to get things working with a single NSOperation that iterates of files and parse one at a time. In this implementation, the -mergeChanges: approach that calls -mergeChangesFromContextDidSaveNotification: on main thread's MOC works well.

But ideally, I would have one NSOperation iterate over source files and spawn NSOperations to parse each file. I have tried several approaches - but can't seem to get it right. The most promising was to have each NSOperation observing NSManagedObjectContextDidSaveNotification. With -mergeChanges: looking like this:

- (void) mergeChanges:(NSNotification *)notification
 {
 // If locally originated, then trigger main thread to merge.
 if ([notification object] == [self managedObjectContext])
  {
  AppDelegate *appDelegate = (AppDelegate*)[[NSApplication sharedApplication] delegate];
  NSManagedObjectContext *mainContext = [appDelegate managedObjectContext];

  // Merge changes into the main context on the main thread
  [mainContext performSelectorOnMainThread:@selector(mergeChangesFromContextDidSaveNotification:)
         withObject:notification
         waitUntilDone:YES];
  return;
  }
   // If not locally originated, then flag need to merge with in this NSOperation's thread.  
 [self setNeedsToMerge:YES];
 [self setMergeNotification:notification];
 }

Essentially, the parsing NSOperation's main() checked ivar 'needsToMerge' periodically. If it was true, then -mergeChangesFromContextDidSaveNotification: was called on local MOC with cached NSNotifications. And then needsToMerge was reset. If notification had originated locally, then main thread was told to perform -mergeChangesFromContextDidSaveNotification: on its MOC.

I am sure that there is a good reason why this didn't work and why I get this:

warning: Cancelling call - objc code on the current thread's stack makes this unsafe.

I have also tried to use NSPeristentStoreCoordinator's lock to control access - but this is problematic if it is held during a call to NSManagedObjectContext's -save: method because -save: will notify interested observers of save event and -mergeChangesFromContextDidSaveNotification: appears to block trying to acquire PSC's lock.

It just seems that this should be a lot easier.

like image 761
westsider Avatar asked Nov 06 '22 04:11

westsider


1 Answers

I think I hade the same problem and here is how I solve it:

Create a custom NSOperation Class where you define:

NSMutableArray * changeNotifications;
NSLock  * changeNotificationsLock;
NSManagedObjectContext  * localManagedObjectContext;

In your NSOperation main method before saving the context first apply all requested changes:

[self.changeNotificationsLock lock];
for(NSNotification * change in self.changeNotifications){
    [self.localManagedObjectContext mergeChangesFromContextDidSaveNotification:change];
}
if([self.changeNotifications count] >0){
    [self.changeNotifications removeAllObjects];
}
[self.changeNotificationsLock unlock];

NSError *error = nil;   
[self.localManagedObjectContext save:&error]

Note that I used a lock, this because NSMutableArray is not thread safe and I want to safely access changeNotifications. changeNotifications is the array where all changes that need to be applied before saving the context are stored.

And here is your merge method, modified so that all changes that need to be merged by your NSOperation are merged using the correct thread. Note that this methods is called by other threads than your NSOperation one, therefore you need to lock the access to self.changeNotifications

- (void) mergeChanges:(NSNotification *)notification
 {
 // If not locally originated, then add notification into change notification array
 // this notification will be treated by the NSOperation thread when needed.
 if ([notification object] != self.localManagedObjectContext)
  {
     [self.changeNotificationsLock lock];
     [self.changeNotifications addObject:notification];
     [self.changeNotificationsLock unlock];
  }

 //Here you may want to trigger the main thread to update the main context     

}

Hope this help! This method is not 100% rock solid there may be some cases where a change notification may arrive too late. In that case the context save method will return an error and you have to reload the NSManagedObject and save it again. In case more details are needed please let me know.

like image 195
Elia Palme Avatar answered Nov 09 '22 07:11

Elia Palme