Sebastian
Sebastian

Reputation: 2942

NSManagedObjectContexts and Multithreading

Let's say we have an app that needs to display a list of places and which runs on 3 Threads:

  1. Main Thread
  2. Main Thread Background Sync (to sync places with a server)
  3. Geocoding thread (to geocode places in the background)

In all 3 threads I have dedicated NSManagedObjectContexts(MOCs). If each MOC can change the underlying data (main thread can e.g. add the place to your favorites, while background sync can change the place's name, while the geocoding thread adds lat/lng information), the app will have to register for the NSManagedObjectContextDidSaveNotification in each thread and then spread the mergeChangesFromContextDidSaveNotification to the corresponding other MOCs in the other threads if one MOC gets saved (not just merge them into the main thread's MOC), right???

Cause right now I am doing that and it doesn't feel right :(

I have a dictionary, which I use to save the currently running threads with their MOCs. Whenever one of the MOCs pops the NSManagedObjectContextDidSaveNotification I loop through this array and send the mergeChangesFromContextDidSaveNotification to all other MOCs/Threads. Of course, I also added an observer to the NSThreadWillExitNotification so that I can remove Thread/MOC from the array when one of the threads runs out. All add/remove actions for the dictionary are locked. And that's where I am kinda stuck right now. Sometimes, when I call

   [moc performSelector:@selector(mergeChangesFromContextDidSaveNotification:) 
onThread:thread 
withObject:notification 
waitUntilDone:YES];

when looping through the MOCs/thread dictionary, I get the following exception thrown at me:

[NSManagedObjectContext performSelector:onThread:withObject:waitUntilDone:modes:]: target thread exited while waiting for the perform

Apparently, this is caused by a race condition. While looping through the dictionary (I only locked it while extracting its objects array), one of the threads within exits and thus the reference is not valid anymore. However, if I put the dictionary lock in front of the entire loop, I get a deadlock, because the call

[moc performSelector:@selector(mergeChangesFromContextDidSaveNotification:) 
onThread:thread 
withObject:notification 
waitUntilDone:YES];

within the loop takes forever in some cases (don't know why yet) and thus causes the entire app to stall. Is it safe to do the call with waitUntilDone:NO in this case? Because that seems to fix it. I just don't know, if I accidentally open pandora's box with this....

Regards,

Sebastian

Upvotes: 2

Views: 2231

Answers (1)

ImHuntingWabbits
ImHuntingWabbits

Reputation: 3827

I think your application structure is putting you in a hazardous situation. It sounds to me like the race condition is that some Thread "A" goes away before you remove the MOC from your storage dictionary. Thus you try to send it a message on Thread "A" and it dies.

Instead think of it this way:

  • Each thread has a thread local object, that you can store inside the thread local storage (see NSThread docs, look for threadDictionary)
  • This object is created when your thread starts doing it's work, and sets up the MOC to be used by a given thread
  • The object also registers for save notifications, and monitors them for the lifetime of the threads work
  • At the end of the threads work this object is destroyed.

This way you encapsulate the life-cycle of the managed object context and directly tie it to the notification mechanism that will reference it. By storing the monitor object in the thread local storage you also tie those two points of concern to the lifetime of the thread.

Upvotes: 1

Related Questions