使核心数据线程安全
长话短说,我厌倦了与NSManagedObjectContext
相关的荒谬的并发规则(或者说,它完全不支持并发性,倾向于爆炸或做其他不正确的事情,如果你试图跨线程共享一个NSManagedObjectContext
),我试图实现一个线程安全的变体。
基本上我所做的是创build一个跟踪它创build的线程的子类,然后将所有方法调用映射回该线程。 这样做的机制有点复杂,但其关键是我有一些帮助方法,如:
- (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]; } }
…然后我用一些代码进行testing:
- (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]; } }
所以基本上,我把一些针对上述入口点的线程加起来,并且随机地创build和删除实体。 这几乎是应该的方式。
问题是每当线程调用obj.<field> = <value>;
时,其中一个线程将得到一个EXC_BAD_ACCESS
obj.<field> = <value>;
。 我不清楚问题是什么,因为如果我在debugging器中打印obj
,一切看起来都不错。 任何build议,可能是什么问题( 除了苹果推荐NSManagedObjectContext的子类 ),以及如何解决它?
PS我知道GCD和NSOperationQueue
和其他技术通常用于“解决”这个问题。 没有一个提供我想要的。 我正在寻找的是一个NSManagedObjectContext
,可以自由,安全地直接使用任何数量的线程查看和更改应用程序状态,而不需要任何外部同步。
正如noa正确地指出,问题是,虽然我已经使NSManagedObjectContext
线程安全,我没有检测NSManagedObject
实例本身是线程安全的。 线程安全上下文和非线程安全实体之间的交互是我的周期性崩溃的原因。
如果有人感兴趣,我创build了一个线程安全的NSManagedObject
子类,通过注入我自己的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, "v@:@"); return YES; } return [super resolveInstanceMethod:sel]; }
这与我的线程安全的上下文实现一起,解决了这个问题,并得到了我想要的; 一个线程安全的上下文,我可以传递给任何我想要的,而不必担心后果。
当然,这不是一个防弹解决scheme,因为我已经发现至less有以下限制:
/* 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. * */
但是,对于大多数典型的中小型项目,我认为线程安全数据层的好处远远超过了这些限制。
为什么不使用提供的并发types之一实例化上下文,并利用performBlock / performBlockAndWait?
这实现了必要的线程限制,不得不与Core Data的访问方法的实现相冲突。 哪一个,你很快就会发现,对于你的用户来说,要么得到正确的结果是非常痛苦的。
Bart Jacobs撰写的一篇很好的教程,题目为: 从头开始的核心数据:那些需要iOS 5.0或更高版本和/或Lion或更高版本的优雅解决scheme的并发性 。 详细描述了两种方法,更优雅的解决scheme涉及父/子pipe理的对象上下文。
- NSFetchedResultsController在合并“NSManagedObjectContextDidSaveNotification”后没有显示所有结果
- 为什么我应该为每个新线程或NSOperation创build一个NSManagedObjectContext,而不是在主线程上调用Core Data?
- dispatch_once过度杀伤了+ ?
- 在不同的线程上调用我的方法有哪些不同的方法?
- Swift 3 GCDlockingvariables和block_and_release错误
- 如果方法结束,只能继续循环
- 用于保存核心数据的NSPersistentContainer并发性
- iOS核心数据何时保存上下文?
- 如何找出是什么导致IOS设备上的错误崩溃报告?