Para resumir, estoy cansado de las absurdas reglas de concurrencia asociadas con NSManagedObjectContext
(o más bien, su completa falta de compatibilidad con la concurrencia y la tendencia a explotar o hacer otras cosas incorrectas si intentas compartir un NSManagedObjectContext
entre hilos), y estoy tratando de implementar una variante segura para subprocesos.Making Core Data Thread-safe
Básicamente, lo que he hecho es crear una subclase que rastrea el hilo en el que se creó y luego asigna todas las invocaciones de método a ese hilo. El mecanismo para hacer esto es un poco enrevesado, pero el quid de la cuestión es que tengo algunos métodos auxiliares como:
- (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;
}
... y luego la subclase implementa la interfaz NSManagedContext
siguiendo un patrón como:
- (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];
}
}
... y luego me estoy probando con algo de código que dice así:
- (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];
}
}
Así que, esencialmente, me giro hasta algunos hilos dirigidos al punto de entrada más arriba, los cuales fueron corriendo Domly crea y elimina entidades. Esto casi funciona como debería.
El problema es que cada cierto tiempo uno de los hilos obtendrá un EXC_BAD_ACCESS
al llamar al obj.<field> = <value>;
. No está claro para mí cuál es el problema, porque si imprimo obj
en el depurador, todo se ve bien. ¿Alguna sugerencia sobre cuál podría ser el problema (que no sea el hecho de que Apple recomienda no crear subclases NSManagedObjectContext) y cómo solucionarlo?
P.S. Estoy al tanto de GCD y NSOperationQueue
y otras técnicas que normalmente se usan para "resolver" este problema. Ninguno de ellos ofrece lo que quiero. Lo que estoy buscando es un NSManagedObjectContext
que pueda ser utilizado de manera libre, segura y directa por cualquier cantidad de subprocesos para ver y cambiar el estado de la aplicación sin requerir ninguna sincronización externa.
¿El problema es que está manipulando atributos en un hilo diferente al del contexto, y posiblemente también al mismo tiempo con otras operaciones en ese contexto, incluyendo guardar y eliminar? Podría intentar redefinir setSomeNumber, establecer AnotherNumber, setAString para ejecutar en el hilo de contexto y ver si eso afecta sus resultados. – paulmelnikow
Sí, parece haberlo estabilizado. Entonces, ahora la pregunta es, ¿cómo creo una subclase 'NSManagedObject' que dinámicamente inyecte implementaciones de implementador de propiedades de thread-safe? – aroth
Tengo funcionando la inyección setter. Es aún más intrincado que los cambios 'NSManagedObjectContext'. Pero lo importante es que funciona. Si alguien está interesado, compartiré la parte relevante del código. – aroth