2012-05-15 90 views
13

长话短说,我厌倦了与NSManagedObjectContext相关的荒谬并发规则(或者说,它完全缺乏对并发性的支持,倾向于爆炸或者如果尝试分享时做其他不正确的事情跨线程的NSManagedObjectContext),并试图实现线程安全变体。使核心数据线程安全

基本上我所做的就是建立追踪,它创建的线程,然后所有的方法调用映射回该线程的子类。这样做的机制是稍微令人费解,但它的关键是,我有一些辅助的方法,如:

- (NSInvocation*) invocationWithSelector:(SEL)selector { 
    //creates an NSInvocation for the given selector 
    NSMethodSignature* sig = [self methodSignatureForSelector:selector];  
    NSInvocation* call = [NSInvocation invocationWithMethodSignature:sig]; 
    [call retainArguments]; 
    call.target = self; 

    call.selector = selector; 

    return call; 
} 

- (void) runInvocationOnContextThread:(NSInvocation*)invocation { 
    //performs an NSInvocation on the thread associated with this context 
    NSThread* currentThread = [NSThread currentThread]; 
    if (currentThread != myThread) { 
     //call over to the correct thread 
     [self performSelector:@selector(runInvocationOnContextThread:) onThread:myThread withObject:invocation waitUntilDone:YES]; 
    } 
    else { 
     //we're okay to invoke the target now 
     [invocation invoke]; 
    } 
} 


- (id) runInvocationReturningObject:(NSInvocation*) call { 
    //returns object types only 
    [self runInvocationOnContextThread:call]; 

    //now grab the return value 
    __unsafe_unretained id result = nil; 
    [call getReturnValue:&result]; 
    return result; 
} 

...然后子类以下类似的模式实现了NSManagedContext接口:

- (NSArray*) executeFetchRequest:(NSFetchRequest *)request error:(NSError *__autoreleasing *)error { 
    //if we're on the context thread, we can directly call the superclass 
    if ([NSThread currentThread] == myThread) { 
     return [super executeFetchRequest:request error:error]; 
    } 

    //if we get here, we need to remap the invocation back to the context thread 
    @synchronized(self) { 
     //execute the call on the correct thread for this context 
     NSInvocation* call = [self invocationWithSelector:@selector(executeFetchRequest:error:) andArg:request]; 
     [call setArgument:&error atIndex:3]; 
     return [self runInvocationReturningObject:call]; 
    } 
} 

...然后我有一些代码,就像去测试它:

- (void) testContext:(NSManagedObjectContext*) context { 
    while (true) { 
     if (arc4random() % 2 == 0) { 
      //insert 
      MyEntity* obj = [NSEntityDescription insertNewObjectForEntityForName:@"MyEntity" inManagedObjectContext:context]; 
      obj.someNumber = [NSNumber numberWithDouble:1.0]; 
      obj.anotherNumber = [NSNumber numberWithDouble:1.0]; 
      obj.aString = [NSString stringWithFormat:@"%d", arc4random()]; 

      [context refreshObject:obj mergeChanges:YES]; 
      [context save:nil]; 
     } 
     else { 
      //delete 
      NSArray* others = [context fetchObjectsForEntityName:@"MyEntity"]; 
      if ([others lastObject]) { 
       MyEntity* target = [others lastObject]; 
       [context deleteObject:target]; 
       [context save:nil]; 
      } 
     } 
     [NSThread sleepForTimeInterval:0.1]; 
    } 
} 

所以基本上,我旋转起来一些线程针对上面的切入点,他们跑了domly创建和删除实体。这几乎按照它应该的方式工作。

的问题是,调用obj.<field> = <value>;时,每隔一段时间一个线程会得到一个EXC_BAD_ACCESS。我不清楚问题是什么,因为如果我在调试器中打印obj,一切看起来都不错。有什么问题可能会有什么建议(,除了Apple建议不要继承NSManagedObjectContext的事实)以及如何解决这个问题?

P.S.我知道GCD和NSOperationQueue和其他技术通常用于“解决”这个问题。这些都不是我想要的。我正在寻找的是NSManagedObjectContext,它可以自由,安全且直接地被任意数量的线程使用,以查看和更改应用程序状态,而无需任何外部同步。

+1

问题是您在不同于上下文的线程上操作属性,因此可能与该上下文中的其他操作(包括保存和删除)同时操作?您可以尝试重写setSomeNumber,setAnotherNumber,setAString以在上下文线程上运行,并查看是否会影响结果。 – paulmelnikow

+0

是的,似乎已经稳定它。所以现在的问题是,我该如何创建一个动态注入线程安全属性设置器实现的NSManagedObject子类? – aroth

+0

我得到了setter注入的东西工作。它比'NSManagedObjectContext'更改更令人费解。但重要的是它的工作原理。如果有人有兴趣,我会分享相关部分的代码。 – aroth

回答

7

正如noa正确地指出的那样,问题是虽然我已经使线程安全,但我没有检测到它们自己是线程安全的。线程安全上下文和非线程安全实体之间的相互作用是我定期崩溃的原因。

如果有人感兴趣,我创建了一个线程安全的子类,通过注入我自己的setter方法来代替(一些)Core Data通常会生成的子类。这是使用类似的代码来完成:

//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 thread 
void 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, "[email protected]:@"); 
     return YES; 
    } 

    return [super resolveInstanceMethod:sel]; 
} 

这与我的线程安全的前提下实现的同时,解决了这个问题,并让我我想要的东西;一个线程安全的上下文,我可以传递给任何我想要的人,而不必担心后果。

当然这一点是不是防弹解决方案,因为我至少已经确定了以下限制:

/* 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. 
* 
*/ 

然而,对于大多数典型的小到中型的项目,我想的好处线程安全数据层显着地超过了这些限制。

+1

非常好。你能把这个放在Github上吗?我相信很多人都会从这样的项目中受益。 – CodaFi

+5

@CodaFi - 它花了一段时间(对不起),但在这里你去:https://github.com/adam-roth/coredata-threadsafe – aroth

+1

图书馆?谢谢。一百万次,谢谢! – CodaFi

3

为什么不直接使用实例所提供的并发类型之一,并充分利用performBlock/performBlockAndWait你的背景?

这与具有核心数据的存取方法的实施,裂伤实现必要的线程限制。哪一个,你很快就会发现,对于你的用户来说,要么得到正确的结果要么很糟糕,要么会很痛苦。

+0

您只能在iOS 5.0及更高版本中指定并发类型(并使用'performBlock')。我需要一个兼容至少4.x的解决方案。 – aroth

+0

如果在iOS 4上:为每个上下文创建自己的队列,并且只使用属于该队列上的上下文的NSManagedObject实例。即使从这些对象中读取,您也只能在该队列中执行该操作。 –

1

由Bart Jacobs撰写的一篇精彩教程,标题为:Core Data from Scratch: Concurrency,适合那些需要iOS 5.0或更高版本和/或Lion或更高版本的优雅解决方案。详细描述了两种方法,更优雅的解决方案涉及父/子管理的对象上下文。