Ответ 1
Как было правильно указано, проблема заключалась в том, что, хотя я сделал поточную безопасность NSManagedObjectContext
, я не использовал теги NSManagedObject
, чтобы они были потокобезопасными. Взаимодействие между потокобезопасным контекстом и объектами, не зависящими от потоков, отвечало за мои периодические сбои.
В случае, если кому-то интересно, я создал потокобезопасный подкласс NSManagedObject
, введя мои собственные методы setter вместо (некоторых) тех, которые Core Generation обычно генерирует. Это выполняется с помощью следующего кода:
//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.
*
*/
Однако для большинства типичных проектов малого и среднего размера я считаю, что преимущества потокобезопасного уровня данных значительно перевешивают эти ограничения.