Making Core Data Thread-safe Making Core Data Thread-safe multithreading multithreading

Making Core Data Thread-safe


As noa rightly pointed out, the problem was that although I had made the NSManagedObjectContext thread-safe, I had not instrumented the NSManagedObject instances themselves to be thread-safe. Interactions between the thread-safe context and the non-thread-safe entities were responsible for my periodic crashes.

In case anyone is interested, I created a thread-safe NSManagedObject subclass by injecting my own setter methods in lieu of (some of) the ones that Core Data would normally generate. This is accomplished using code like:

//implement these so that we know what thread our associated context is on- (void) awakeFromInsert {    myThread = [NSThread currentThread];}- (void) awakeFromFetch {    myThread = [NSThread currentThread];}//helper for re-invoking the dynamic setter method, because the NSInvocation requires a @selector and dynamicSetter() isn't one- (void) recallDynamicSetter:(SEL)sel withObject:(id)obj {    dynamicSetter(self, sel, obj);}//mapping invocations back to the context thread- (void) runInvocationOnCorrectThread:(NSInvocation*)call {    if (! [self myThread] || [NSThread currentThread] == [self myThread]) {        //okay to invoke        [call invoke];    }    else {        //remap to the correct thread        [self performSelector:@selector(runInvocationOnCorrectThread:) onThread:myThread withObject:call waitUntilDone:YES];    }}//magic!  perform the same operations that the Core Data generated setter would, but only after ensuring we are on the correct threadvoid dynamicSetter(id self, SEL _cmd, id obj) {    if (! [self myThread] || [NSThread currentThread] == [self myThread]) {        //okay to execute        //XXX:  clunky way to get the property name, but meh...        NSString* targetSel = NSStringFromSelector(_cmd);        NSString* propertyNameUpper = [targetSel substringFromIndex:3];  //remove the 'set'        NSString* firstLetter = [[propertyNameUpper substringToIndex:1] lowercaseString];        NSString* propertyName = [NSString stringWithFormat:@"%@%@", firstLetter, [propertyNameUpper substringFromIndex:1]];        propertyName = [propertyName substringToIndex:[propertyName length] - 1];        //NSLog(@"Setting property:  name=%@", propertyName);        [self willChangeValueForKey:propertyName];        [self setPrimitiveValue:obj forKey:propertyName];        [self didChangeValueForKey:propertyName];    }    else {        //call back on the correct thread        NSMethodSignature* sig = [self methodSignatureForSelector:@selector(recallDynamicSetter:withObject:)];        NSInvocation* call = [NSInvocation invocationWithMethodSignature:sig];        [call retainArguments];        call.target = self;        call.selector = @selector(recallDynamicSetter:withObject:);        [call setArgument:&_cmd atIndex:2];        [call setArgument:&obj atIndex:3];        [self runInvocationOnCorrectThread:call];    }}//bootstrapping the magic; watch for setters and override each one we see+ (BOOL) resolveInstanceMethod:(SEL)sel {    NSString* targetSel = NSStringFromSelector(sel);    if ([targetSel startsWith:@"set"] && ! [targetSel contains:@"Primitive"]) {        NSLog(@"Overriding selector:  %@", targetSel);        class_addMethod([self class], sel, (IMP)dynamicSetter, "v@:@");        return YES;    }    return [super resolveInstanceMethod:sel];}

This, in conjunction with my thread-safe context implementation, solved the problem and got me what I wanted; a thread-safe context that I can pass around to whomever I want without having to worry about the consequences.

Of course this is not a bulletproof solution, as I have identified at least the following limitations:

/* Also note that using this tool carries several small caveats: * *      1.  All entities in the data model MUST inherit from 'ThreadSafeManagedObject'.  Inheriting directly from  *          NSManagedObject is not acceptable and WILL crash the app.  Either every entity is thread-safe, or none  *          of them are. * *      2.  You MUST use 'ThreadSafeContext' instead of 'NSManagedObjectContext'.  If you don't do this then there  *          is no point in using 'ThreadSafeManagedObject' (and vice-versa).  You need to use the two classes together,  *          or not at all.  Note that to "use" ThreadSafeContext, all you have to do is replace every [[NSManagedObjectContext alloc] init] *          with an [[ThreadSafeContext alloc] init]. * *      3.  You SHOULD NOT give any 'ThreadSafeManagedObject' a custom setter implementation.  If you implement a custom  *          setter, then ThreadSafeManagedObject will not be able to synchronize it, and the data model will no longer  *          be thread-safe.  Note that it is technically possible to work around this, by replicating the synchronization *          logic on a one-off basis for each custom setter added. * *      4.  You SHOULD NOT add any additional @dynamic properties to your object, or any additional custom methods named *          like 'set...'.  If you do the 'ThreadSafeManagedObject' superclass may attempt to override and synchronize  *          your implementation. * *      5.  If you implement 'awakeFromInsert' or 'awakeFromFetch' in your data model class(es), thne you MUST call  *          the superclass implementation of these methods before you do anything else. * *      6.  You SHOULD NOT directly invoke 'setPrimitiveValue:forKey:' or any variant thereof.   * */

However, for most typical small to medium-sized projects I think the benefits of a thread-safe data layer significantly outweigh these limitations.


Why not just instantiate your context using one of the provided concurrency types, and leverage performBlock / performBlockAndWait?

That implements the necessary thread confinement with having to mangle with the implementation of Core Data's accessor methods. Which, as you will soon find out will be either very painful to get right or end quite badly for your users.


A great tutorial by Bart Jacobs entitled: Core Data from Scratch: Concurrency for those that need an elegant solution for iOS 5.0 or later and/or Lion or later. Two approaches are described in detail, the more elegant solution involves parent/child managed object contexts.