2012-05-15 38 views
13

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.

+1

¿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

+0

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

+0

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

Respuesta

7

Como NOA ha señalado acertadamente, el problema fue que, aunque había tomado la NSManagedObjectContext thread-safe, no había instrumentado los NSManagedObject casos sí mismos para ser thread-safe. Las interacciones entre el contexto seguro para subprocesos y las entidades no seguras para subprocesos fueron las responsables de mis bloqueos periódicos.

En caso de que alguien esté interesado, creé una subclase NSManagedObject con subprocesos inyectando mis propios métodos setter en lugar de (algunos) los que Core Data normalmente generaría. Esto se logra utilizando un código como:

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

Esto, junto con mi implementación de contexto seguro para subprocesos, resolvió el problema y me consiguió lo que quería; un contexto seguro para subprocesos que puedo pasar a quien yo quiera sin tener que preocuparme por las consecuencias.

Por supuesto, esto no es una solución a prueba de balas, como he identificado al menos las siguientes limitaciones:

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

Sin embargo, para la mayoría de pequeñas dimensiones para proyectos de tamaño mediano Creo que los beneficios de un seguro para subprocesos la capa de datos supera significativamente estas limitaciones.

+1

Excelente. ¿Podrías poner esto en Github, por favor? Estoy seguro de que muchos se beneficiarían de tal proyecto. – CodaFi

+5

@CodaFi - Me tomó un tiempo (lo siento), pero aquí tienes: https://github.com/adam-roth/coredata-threadsafe – aroth

+1

¿Una biblioteca? Gracias. ¡Un millón de veces, gracias! – CodaFi

3

¿Por qué no simplemente crear una instancia de su contexto utilizando uno de los tipos de concurrencia proporcionados y aprovechar performBlock/performBlockAndWait?

Que implementa el confinamiento de hilo necesario con tener que pasar por la implementación de los métodos de acceso de Core Data. Lo cual, como pronto descubrirás, será muy doloroso acertar o terminar bastante mal para tus usuarios.

+0

Solo puede especificar el tipo de concurrencia (y usar 'performBlock') en iOS 5.0 y posterior. Necesito una solución que sea compatible con al menos 4.x. – aroth

+0

Si está en iOS 4: cree su propia cola para cada contexto y solo use las instancias NSManagedObject que pertenecen a ese contexto en esa cola. Incluso cuando lee de esos objetos, solo puede hacer eso en esa cola. –

1

Un gran tutorial de Bart Jacobs titulado: Core Data from Scratch: Concurrency para aquellos que necesitan una solución elegante para iOS 5.0 o posterior y/o Lion o posterior. Se describen dos enfoques en detalle, la solución más elegante implica contextos de objetos gestionados padres/hijos.