长篇小说,我厌倦了与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];
}
}
所以本质上,我提出了一些针对上述入口点的线程,并且它们随机创建和删除实体.这几乎是应该的方式.
问题是,在调用obj时,经常有一个线程将获得一个EXC_BAD_ACCESS.< field> =< value> ;;.对我来说不清楚是什么问题,因为如果我在调试器中打印obj,一切都看起来不错.关于什么问题可能是什么的建议(除了苹果推荐对NSManagedobjectContext进行子类化的事实)以及如何解决? 附:我知道GCD和NSOperationQueue以及其他通常用来“解决”这个问题的技术.没有一个提供我想要的.我正在寻找的是一个NSManagedobjectContext,可以自由,安全和直接地使用任何数量的线程来查看和更改应用程序状态,而不需要任何外部同步.
解决方法
正如noa正确指出的那样,问题是尽管我已经使NSManagedobjectContext线程安全,但我没有将NSManagedobject实例本身检测为线程安全.线程安全上下文和非线程安全实体之间的交互对我的定期崩溃负责.
如果有人感兴趣,我创建了一个线程安全的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],(IMP)dynamicSetter,"v@:@");
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. * */
然而,对于大多数典型的中小型项目,我认为线程安全数据层的好处明显超过了这些限制.