xCode 7.0 IOS9 SDK: interlocking mientras se ejecuta la request de recuperación con performBlockAndWait

Actualizado: He preparado la muestra que reproduce el problema sin logging mágico. Descargue el proyecto de testing usando la siguiente URL: https://www.dsr-company.com/fm.php?Download=1&FileToDL=DeadLockTest_CoreDataWithoutMR.zip

El proyecto proporcionado tiene el siguiente problema: punto muerto en la búsqueda en performBlockAndWait llamado desde el hilo principal.

El problema se reproduce si el código se comstack utilizando la versión XCode> 6.4. El problema no se reproduce si el código se comstack usando xCode == 6.4.

La vieja pregunta era:

Estoy trabajando en el soporte de la aplicación mobile IOS. Después de la reciente actualización de Xcode IDE de la versión 6.4 a la versión 7.0 (con soporte de IOS 9) me he enfrentado con un problema crítico: el locking de aplicaciones. La misma compilation de la aplicación (producida desde las mismas fonts) con xCode 6.4 funciona bien. Por lo tanto, si la aplicación se crea con xCode> 6.4, la aplicación se cuelga en algunos casos. si la aplicación está construida con xCode 6.4, la aplicación funciona bien.

He dedicado algún time a investigar el problema y, como resultado, he preparado la aplicación de testing con un caso similar al igual que en mi aplicación, que reproduce el problema. El hangup de la aplicación de testing en el Xcode> = 7.0 pero funciona correctamente en el Xcode 6.4

Enlace de descarga de las fonts de testing: https://www.sendspace.com/file/r07cln

Los requisitos para la aplicación de testing son: 1. el administrador de pods de cocoa debe estar instalado en el sistema 2. el esquema de MagicalRecord de la versión 2.2.

La aplicación de testing funciona de la siguiente manera: 1. Al inicio de la aplicación crea una database de testing con 10000 loggings de entidades simples y los guarda en una tienda persistente. 2. En la primera pantalla de la aplicación en el método viewWillAppear: ejecuta la testing que causa un interlocking. El siguiente algorithm se usa:

-(NSArray *) entityWithId: (int) entityId inContext:(NSManagedObjectContext *)localContext { NSArray * results = [TestEntity MR_findByAttribute:@"id" withValue:[ NSNumber numberWithInt: entityId ] inContext:localContext]; return results; } ….. int entityId = 88; NSManagedObjectContext *childContext1 = [NSManagedObjectContext MR_context]; childContext1.name = @"childContext1"; NSManagedObjectContext *childContext2 = [NSManagedObjectContext MR_context]; childContext2.name = @"childContext2"; NSArray *results = [self entityWithId:entityId inContext: childContext2]; for(TestEntity *d in results) { NSLog(@"e from fetchRequest %@ with name = '%@'", d, d.name); /// this line is the reason of the hangup } dispatch_async(dispatch_get_main_queue(), ^ { int entityId2 = 11; NSPnetworkingicate *pnetworkingicate2 = [NSPnetworkingicate pnetworkingicateWithFormat:@"id=%d", entityId2]; NSArray *a = [ TestEntity MR_findAllWithPnetworkingicate: pnetworkingicate2 inContext: childContext2]; for(TestEntity *d in a) { NSLog(@"e from fetchRequest %@ with name = '%@'", d, d.name); } }); 

Se crean dos contexts de objects gestionados con tipo de concurrency == NSPrivateQueueConcurrencyType (verifique el código de MR_context de marco de logging mágico). Ambos contexts tienen un context padre con concurrency type = NSMainQueueConcurrencyType. Desde la aplicación de subprocess principal realiza búsqueda de manera sincronizada (MR_findByAttribute y MR_findAllWithPnetworkingicate se utilizan performBlockAndWait con petición de recuperación dentro). Después de la primera búsqueda, la segunda búsqueda se progtwig en el hilo principal usando dispatch_async ().

Como resultado, la aplicación cuelga. Parece que ha habido un punto muerto, por favor revisa la captura de pantalla de la stack:

 aquí está el enlace, mi reputación es demasiado baja para publicar imágenes. https://cdn.img42.com/34a8869bd8a5587222f9903e50b762f9.png )

Si comentar la línea
NSLog (@ "e de fetchRequest% @ con nombre = '% @'", d, d.nombre); /// esta línea es la razón del hangup

(que es la línea 39 en ViewController.m del proyecto de testing), la aplicación funciona correctamente. Creo que esto se debe a que no hay campo de lectura del nombre de la entidad de testing.

Entonces, con la línea comentada NSLog (@ "e de fetchRequest% @ con nombre = '% @'", d, d.name);
no hay ningún hangup en los binarys construidos con Xcode 6.4 y Xcode 7.0.

Con la línea no comentada NSLog (@ "e de fetchRequest% @ con nombre = '% @'", d, d.nombre);

hay un hangup en binary construido con Xcode 7.0 y no hay un hangup en binary construido con Xcode 6.4.

Creo que el problema ocurre debido a la carga perezosa de los datos de la entidad.

¿Alguien tiene algún problema con el caso descrito? Estaré agradecido por cualquier ayuda.

Es por eso que no uso frameworks que resumen (es decir, ocultan) demasiados detalles de los datos centrales. Tiene patrones de uso muy complejos y, a veces, necesita conocer los detalles de cómo interactúan.

Primero, no sé nada sobre el logging mágico, excepto que mucha gente lo usa, por lo que debe ser bastante bueno en lo que hace.

Sin embargo, inmediatamente vi varios usos erróneos de la concurrency de datos centrales en sus ejemplos, así que fui y miré los files del encabezado para ver por qué su código hacía las suposiciones que hacía.

No me refiero a golpearte en absoluto, aunque esto pueda parecer a primera vista. Quiero ayudar a educarte (y usé esto como una oportunidad para echar un vistazo a MR).

Desde un vistazo muy rápido a MR, diría que tiene algunos malentendidos de lo que hace MR y también las reglas generales de concurrency de los datos básicos.

Primero dices esto …

Se crean dos contexts de objects gestionados con tipo de concurrency == NSPrivateQueueConcurrencyType (verifique el código de MR_context de marco de logging mágico). Ambos contexts tienen un context padre con concurrency type = NSMainQueueConcurrencyType.

que no parece ser cierto. Los dos nuevos contexts son, de hecho, contexts de queue privada, pero su padre (según el código que he visto en github) es el mágico MR_rootSavingContext , que a su vez es también un context de queue privada.

Analicemos su ejemplo de código.

 NSManagedObjectContext *childContext1 = [NSManagedObjectContext MR_context]; childContext1.name = @"childContext1"; NSManagedObjectContext *childContext2 = [NSManagedObjectContext MR_context]; childContext2.name = @"childContext2"; 

Entonces, ahora tiene dos MOC de queue privada ( childContext1 y childContext2 ), hijos de otro MOC anónimo de queue privada (llamaremos a savingContext ).

 NSArray *results = [self entityWithId:entityId inContext: childContext2]; 

A continuación, realiza una búsqueda en childContext1 . Ese código es en realidad …

 -(NSArray *) entityWithId:(int)entityId inContext:(NSManagedObjectContext *)localContext { NSArray * results = [TestEntity MR_findByAttribute:@"id" withValue:[NSNumber numberWithInt:entityId] inContext:localContext]; return results; } 

Ahora, sabemos que localContext en este método es, en este caso, otro puntero a childContext2 que es un MOC de queue privada. Es 100% en contra de las reglas de concurrency para acceder a un MOC de queue privada fuera de una llamada a performBlock . Sin embargo, dado que está utilizando otra API, y el nombre del método no ofrece ayuda para saber cómo se accede al MOC, debemos ir a esa API y ver si oculta el performBlock de performBlock para ver si está accediendo correctamente.

Desafortunadamente, la documentation en el file de encabezado no ofrece ninguna indicación, por lo que debemos observar la implementación. Esa llamada termina llamando a MR_executeFetchRequest... lo que no indica en la documentation cómo maneja la concurrency tampoco. Entonces, vamos a ver su implementación.

Ahora, estamos llegando a algún lado. Esta function intenta acceder de forma segura al MOC, pero usa performBlockAndWait que bloqueará cuando se llame.

Esta es una información extremadamente importante, porque llamarlo desde el lugar equivocado puede causar un punto muerto. Por lo tanto, debe ser muy consciente de que performBlockAndWait está performBlockAndWait cada vez que ejecuta una request de recuperación. Mi propia regla personal es nunca usar performBlockAndWait less que no haya absolutamente ninguna otra opción.

Sin embargo, esta llamada aquí debe ser completamente segura … suponiendo que no se la llame desde el context del MOC principal.

Entonces, veamos la siguiente pieza de código.

 for(TestEntity *d in results) { NSLog(@"e from fetchRequest %@ with name = '%@'", d, d.name); /// this line is the reason of the hangup } 

Ahora, esto no es culpa de MagicalRecord, porque MR ni siquiera se usa directamente aquí. Sin embargo, ha sido entrenado para usar esos methods MR_ , que no requieren conocimiento del model de concurrency, por lo que olvida o nunca aprende las reglas de concurrency.

Los objects en la matriz de results son todos los objects gestionados que viven en el context de queue privada childContext2 . Por lo tanto, no puede acceder a ellos sin rendir homenaje a las reglas de concurrency. Esta es una clara violación de las reglas de concurrency. Al desarrollar su aplicación, debe habilitar la debugging de la concurrency con el argumento -com.apple.CoreData.ConcurrencyDebug 1.

Este fragment de código debe envolverse en performBlock o performBlockAndWait . Casi nunca utilizo performBlockAndWait para nada porque tiene muchos inconvenientes: los puntos muertos son uno de ellos. De hecho, solo ver el uso de performBlockAndWait es una indicación muy fuerte de que su punto muerto está sucediendo allí y no en la línea de código que usted indica. Sin embargo, en este caso, es al less tan seguro como la captura anterior, así que hagámoslo un poco más seguro …

 [childContext2 performBlockAndWait:^{ for (TestEntity *d in results) { NSLog(@"e from fetchRequest %@ with name = '%@'", d, d.name); } }]; 

Luego, envía al hilo principal. ¿Eso es porque simplemente quieres que ocurra algo en un ciclo de ciclo de evento posterior o es porque este código ya se está ejecutando en algún otro subprocess? Quién sabe. Sin embargo, usted tiene el mismo problema aquí (he reformateado su código para la legibilidad como una publicación).

 dispatch_async(dispatch_get_main_queue(), ^{ int entityId2 = 11; NSPnetworkingicate *pnetworkingicate2 = [NSPnetworkingicate pnetworkingicateWithFormat:@"id=%d", entityId2]; NSArray *a = [TestEntity MR_findAllWithPnetworkingicate:pnetworkingicate2 inContext:childContext2]; for (TestEntity *d in a) { NSLog(@"e from fetchRequest %@ with name = '%@'", d, d.name); } }); 

Ahora, sabemos que el código comienza a ejecutarse en el hilo principal y la búsqueda llamará a performBlockAndWait pero su acceso posterior en el bucle performBlockAndWait infringe nuevamente las reglas de concurrency de datos del núcleo.

Basado en eso, los únicos problemas reales que veo son …

  1. MR parece honrar las reglas de concurrency de datos centrales dentro de su API, pero aún debe seguir las reglas de concurrency de datos centrales al acceder a sus objects gestionados.

  2. Realmente no me gusta el uso de performBlockAndWait ya que es solo un problema a la espera de suceder.

Ahora, echemos un vistazo a la captura de pantalla de su locking. Hmmm … es un estancamiento clásico, pero no tiene sentido porque el punto muerto ocurre entre el hilo principal y el hilo MOC. Eso solo puede suceder si el MOC de queue principal es uno de los padres de este MOC de queue privada, pero el código muestra que no es así.

Hmmm … no tenía sentido, así que descargué tu proyecto y miré el código fuente en el pod que subiste. Ahora, esa versión del código usa el MR_defaultContext como el padre de todos los MOC creados con MR_context . Por lo tanto, el MOC pnetworkingeterminado es, de hecho, un MOC de queue principal, y ahora todo tiene mucho sentido.

Tiene un MOC como hijo de un MOC de queue principal. Cuando envía ese bloque a la queue principal, ahora se está ejecutando como un bloque en la queue principal. A continuación, el código llama a performBlockAndWait en un context que es hijo de un MOC para esa queue, que es un gran no-no, y casi está garantizado que obtendrá un punto muerto.

Por lo tanto, parece que MR ha cambiado su código de usar una queue principal como el padre de los nuevos contexts para usar una queue privada como el padre de los nuevos contexts (probablemente debido a este problema exacto). Por lo tanto, si actualiza a la última versión de MR, debería estar bien.

Sin embargo, todavía le advierto que si desea utilizar MR en forms multiprocess, debe saber exactamente cómo manejan las reglas de concurrency, y también debe asegurarse de obedecerlas cada vez que acceda a cualquier object de datos de núcleo que no sea pasando por la MR API.

Finalmente, solo diré que he hecho toneladas y toneladas de datos básicos, y nunca he usado una API que intente ocultar los problemas de concurrency de mí. La razón es que hay demasiados pequeños casos en las esquinas, y preferiría lidiar con ellos de manera pragmática desde el principio.

Finalmente, casi nunca debería usar performBlockAndWait less que sepa exactamente por qué es la única opción. Tenerlo como parte de una API debajo de ti es incluso más aterrador … para mí al less.

Espero que esta pequeña excursión te haya iluminado y te haya ayudado (y posiblemente a otros). Ciertamente, derramó un poco de luz para mí, y ayudó a restablecer algo de mi desconfianza infundada previa.

Editar

Esto es en respuesta al ejemplo de "logging no mágico" que proporcionó.

El problema con este código es exactamente el mismo problema que describí anteriormente, relativo a lo que estaba sucediendo con MR.

Tiene un context de queue privada, como un elemento secundario en un context de queue principal.

Está ejecutando código en la queue principal y llama a performBlockAndWait en el context secundario, que luego debe bloquear su context principal cuando intenta ejecutar la búsqueda.

Se llama un punto muerto, pero el término más descriptivo (y seductor) es el aarm mortal.

El código original se ejecuta en el hilo principal. Convoca a un context infantil para hacer algo, y no hace nada más hasta que ese niño complete.

Ese niño entonces, para completar, necesita el hilo principal para hacer algo. Sin embargo, el hilo principal no puede hacer nada hasta que el niño haya terminado … pero el niño está esperando que el hilo principal haga algo …

Ninguno puede avanzar.

El problema que usted enfrenta está muy bien documentado y, de hecho, se lo mencionó varias veces en las presentaciones de WWDC y en múltiples documentos.

NUNCA debe llamar a performBlockAndWait en un context secundario.

El hecho de que te saliste con la suya en el pasado es solo una "casualidad" porque no se supone que funcione de esa manera en absoluto.

En realidad, casi no debería realizar cada llamada performBlockAndWait .

Realmente deberías acostumbrarte a hacer una progtwigción asíncrona. Así es como recomendaría que reescriba esta testing, y sea lo que sea que le haya llamado a este problema.

Primero, vuelva a escribir la búsqueda para que funcione de forma asíncrona …

 - (void)executeFetchRequest:(NSFetchRequest *)request inContext:(NSManagedObjectContext *)context completion:(void(^)(NSArray *results, NSError *error))completion { [context performBlock:^{ NSError *error = nil; NSArray *results = [context executeFetchRequest:request error:&error]; if (completion) { completion(results, error); } }]; } 

Entonces, cambias el código que llama a la búsqueda para hacer algo como esto …

 NSFetchRequest *request = [[NSFetchRequest alloc] init]; [request setEntity: testEntityDescription ]; [request setPnetworkingicate: pnetworkingicate2 ]; [self executeFetchRequest:request inContext:childContext2 completion:^(NSArray *results, NSError *error) { if (results) { for (TestEntity *d in results) { NSLog(@"++++++++++ e from fetchRequest %@ with name = '%@'", d, d.name); } } else { NSLog(@"Handle this error: %@", error); } }]; 

Cambiamos a XCode7 y me encontré con un problema de interlocking similar con performBlockAndWait en el código que funciona bien en XCode6.

El problema parece ser un uso ascendente de dispatch_async(mainQueue, ^{ ... para pasar el resultado de una operación de networking. Esa llamada ya no era necesaria después de agregar compatibilidad de concurrency para CoreData, pero de alguna manera se dejó y nunca pareció para causar un problema hasta ahora.

Es posible que Apple haya cambiado algo detrás de escena para hacer que los potenciales interlockings sean más explícitos.