2012-05-15 45 views
13

Câu chuyện dài ngắn, tôi mệt mỏi với các quy tắc đồng thời vô lý liên quan đến NSManagedObjectContext (hoặc đúng hơn, thiếu hoàn toàn hỗ trợ đồng thời và xu hướng phát nổ hoặc làm những việc không chính xác khác nếu bạn cố gắng chia sẻ một số NSManagedObjectContext trên toàn bộ chủ đề) và đang cố gắng triển khai một biến thể an toàn chủ đề.Làm cho dữ liệu cốt lõi an toàn chủ đề

Về cơ bản những gì tôi đã làm được tạo ra một phân lớp theo dõi chuỗi mà nó được tạo ra, và sau đó ánh xạ tất cả các lời gọi phương thức trở lại chủ đề đó. Cơ chế để làm điều này là hơi phức tạp, nhưng mấu chốt của nó là tôi đã có một số phương pháp helper như:

- (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; 
} 

... và sau đó là lớp con thực hiện các giao diện NSManagedContext sau một mô hình như:

- (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]; 
    } 
} 

... và sau đó tôi thử nghiệm nó với một số mã mà đi như:

- (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]; 
    } 
} 

Vì vậy, về cơ bản, tôi quay lên một số đề nhắm mục tiêu các điểm nhập cảnh trên, và họ chạy domly tạo và xóa các thực thể. Điều này gần như hoạt động theo cách nó nên.

Vấn đề là mỗi thường một trong các chủ đề sẽ nhận được EXC_BAD_ACCESS khi gọi obj.<field> = <value>;. Nó không rõ ràng với tôi vấn đề là gì, bởi vì nếu tôi in obj trong trình gỡ lỗi, mọi thứ đều tốt. Bất kỳ đề xuất nào về vấn đề có thể là gì (khác với thực tế là Apple khuyến nghị chống lại phân lớp NSManagedObjectContext) và cách khắc phục sự cố?

P.S. Tôi biết về GCD và NSOperationQueue và các kỹ thuật khác thường được sử dụng để "giải quyết" sự cố này. Không ai trong số những người cung cấp những gì tôi muốn. Những gì tôi đang tìm kiếm là NSManagedObjectContext có thể được tự do, an toàn và được sử dụng trực tiếp bởi bất kỳ số luồng nào để xem và thay đổi trạng thái ứng dụng mà không yêu cầu bất kỳ đồng bộ hóa bên ngoài nào.

+1

Có phải vấn đề mà bạn đang thao tác các thuộc tính trên một luồng khác với ngữ cảnh và do đó có thể đồng thời với các thao tác khác trên ngữ cảnh đó, bao gồm lưu và xóa? Bạn có thể thử ghi đè setSomeNumber, setAnotherNumber, setAString để chạy trên chuỗi ngữ cảnh và xem liệu điều đó có ảnh hưởng đến kết quả của bạn hay không. – paulmelnikow

+0

Có, dường như đã ổn định nó. Vì vậy, bây giờ câu hỏi là, làm thế nào để tôi tạo ra một lớp con 'NSManagedObject' tự động tiêm triển khai setter thuộc tính thread-safe? – aroth

+0

Tôi nhận được công cụ tiêm setter hoạt động. Nó thậm chí còn phức tạp hơn những thay đổi 'NSManagedObjectContext'. Nhưng điều quan trọng là nó hoạt động. Nếu có ai quan tâm, tôi sẽ chia sẻ phần mã tương ứng. – aroth

Trả lời

7

Như đã được chỉ ra một cách đúng đắn, vấn đề là mặc dù tôi đã thực hiện an toàn chủ đề an toàn NSManagedObjectContext, nhưng tôi chưa tự thiết lập các cá thể NSManagedObject an toàn. Tương tác giữa bối cảnh an toàn chủ đề và các thực thể không an toàn chủ đề chịu trách nhiệm về các sự cố định kỳ của tôi.

Trong trường hợp bất kỳ ai quan tâm, tôi đã tạo một lớp con an toàn NSManagedObject bằng cách tiêm các phương thức setter riêng thay cho (một số) phương thức mà Dữ liệu cốt lõi thường tạo ra. Điều này được thực sử dụng mã như:

//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]; 
} 

này, kết hợp với thread-safe thực hiện bối cảnh của tôi, giải quyết vấn đề và đã cho tôi những gì tôi muốn; một bối cảnh an toàn cho chủ đề mà tôi có thể truyền đạt cho bất cứ ai mà tôi muốn mà không phải lo lắng về hậu quả.

Tất nhiên đây không phải là một giải pháp chống đạn, như tôi đã xác định được ít nhất là hạn chế sau đây:

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

Tuy nhiên, đối tiêu biểu nhất nhỏ cho các dự án vừa và nhỏ Tôi nghĩ rằng những lợi ích của một thread-safe lớp dữ liệu lớn hơn đáng kể những hạn chế này.

+1

Tuyệt vời. Bạn có thể đặt cái này lên Github được không? Tôi chắc rằng nhiều người sẽ được hưởng lợi từ một dự án như vậy. – CodaFi

+5

@CodaFi - Mất một lúc (xin lỗi về điều đó), nhưng ở đây bạn đi: https://github.com/adam-roth/coredata-threadsafe – aroth

+1

Một thư viện? Cảm ơn bạn. Một triệu lần, cảm ơn bạn! – CodaFi

3

Tại sao không chỉ khởi tạo ngữ cảnh của bạn bằng một trong các loại đồng thời được cung cấp và tận dụng performBlock/performBlockAndWait?

Điều đó thực hiện việc giữ chỉ định cần thiết khi phải mangle với việc triển khai các phương thức truy cập của Dữ liệu cốt lõi. Mà, như bạn sẽ sớm tìm ra sẽ rất đau đớn để có được quyền hoặc kết thúc khá nặng cho người dùng của bạn.

+0

Bạn chỉ có thể chỉ định loại đồng thời (và sử dụng 'performBlock') trên iOS 5.0 trở lên. Tôi cần một giải pháp tương thích với ít nhất 4.x. – aroth

+0

Nếu trên iOS 4: Tạo hàng đợi của riêng bạn cho từng ngữ cảnh và chỉ sử dụng các cá thể NSManagedObject thuộc ngữ cảnh đó trên hàng đợi đó. Ngay cả khi đọc từ những đối tượng đó bạn chỉ có thể làm điều đó trên hàng đợi đó. –

1

Hướng dẫn tuyệt vời của Bart Jacobs có tựa đề: Core Data from Scratch: Concurrency dành cho những người cần giải pháp thanh lịch cho iOS 5.0 trở lên và/hoặc Lion trở lên. Hai cách tiếp cận được mô tả chi tiết, giải pháp thanh lịch hơn bao gồm bối cảnh đối tượng được quản lý cha/con.

Các vấn đề liên quan