2012-05-15 128 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或更高版本的優雅解決方案。詳細描述了兩種方法,更優雅的解決方案涉及父/子管理的對象上下文。