Unlimited Plugins, WordPress themes, videos & courses! Unlimited asset downloads! From $16.50/m
Advertisement
  1. Code
  2. Core Data
Code

Core Data desde cero: Concurrencia

by
Difficulty:IntermediateLength:LongLanguages:
This post is part of a series called Core Data from Scratch.
Core Data from Scratch: Subclassing NSManagedObject
iOS 8: Core Data and Batch Updates

Spanish (Español) translation by Elías Nicolás (you can also view the original English article)

Si está desarrollando una aplicación pequeña o simple, entonces probablemente no vea el beneficio de ejecutar las operaciones de Core Data en segundo plano. Sin embargo, ¿qué pasaría si importó cientos o miles de registros en el hilo principal durante el primer lanzamiento de su aplicación? Las consecuencias pueden ser dramáticas. Por ejemplo, su aplicación podría ser eliminada por el watchdog de Apple por demorarse demasiado en el lanzamiento.

En este artículo, analizamos los peligros al usar Core Data en varios hilos y exploramos varias soluciones para abordar el problema.

1. Seguridad de subprocesos

Cuando se trabaja con datos centrales, es importante recordar siempre que Core Data no es seguro para subprocesos. Core Data espera ejecutarse en un solo hilo. Esto no significa que cada operación de Core Data deba realizarse en el hilo principal, lo cual es cierto para UIKit, pero sí significa que debe saber qué operaciones se ejecutan en qué subprocesos. También significa que debe tener cuidado de cómo los cambios de un hilo se propagan a otros hilos.

Trabajar con datos centrales en múltiples hilos es realmente muy simple desde un punto de vista teórico. NSManagedObject, NSManagedObjectContext, y NSPersistentStoreCoordinator no son seguros para la ejecución de subprocesos y solo se puede acceder a las instancias de estas clases desde el subproceso en el que se crearon. Como se puede imaginar, esto se vuelve un poco más complejo en la práctica.

NSManagedObject

Ya sabemos que la clase NSManagedObject no es segura para subprocesos, pero ¿cómo se accede a un registro desde diferentes subprocesos? Una instancia de NSManagedObject tiene un método objectID que devuelve una instancia de la clase NSManagedObjectID. La clase NSManagedObjectID es segura para subprocesos y una instancia contiene toda la información que necesita un contexto de objeto gestionado para recuperar el objeto gestionado correspondiente.

En el siguiente fragmento de código, solicitamos un contexto de objeto gestionado para el objeto gestionado que se corresponde con objectID. Los objectWithID: y existingObjectWithID: error: los métodos devuelven una versión local-local al hilo actual-del objeto gestionado correspondiente.

La regla básica para recordar es no pasar la instancia de NSManagedObject de un hilo a otro. En su lugar, pase  objectID del objeto gestionado y pregunte al contexto del objeto gestionado del subproceso por una versión local del objeto gestionado.

NSManagedObjectContext

Como la clase NSManagedObjectContext no es segura para subprocesos, podríamos crear un contexto de objeto gestionado para cada subproceso que interactúe con Core Data. Esta estrategia a menudo se conoce como confinamiento de hilo.

Un enfoque común es almacenar el contexto del objeto gestionado en el diccionario del hilo, un diccionario para almacenar datos específicos del hilo. Eche un vistazo al siguiente ejemplo para ver cómo funciona esto en la práctica.

NSPersistentStoreCoordinator

¿Qué hay del persistent store coordinator? ¿Necesita crear un persistent store coordinator por separado para cada hilo. Si bien esto es posible y una de las estrategias que Apple solía recomendar, no es necesario.

La clase NSPersistentStoreCoordinator se diseñó para admitir múltiples contextos de objetos gestionados, incluso si esos contextos se crearon en diferentes subprocesos. Como la clase NSManagedObjectContext bloquea el coordinador de guardado persistente mientras se accede a ella, es posible que múltiples contextos de objetos gestionados utilicen el mismo persistent store coordinator, incluso si esos contextos de objetos gestionados viven en procesos diferentes. Esto hace que una configuración de Core Data multiproceso sea mucho más manejable y menos compleja.

2. Estrategias de concurrencia

Hasta ahora, hemos aprendido que necesita múltiples contextos de objetos gestionados si realiza operaciones de Datos principales en varios hilos. La advertencia, sin embargo, es que los contextos de objetos gestionados no son conscientes de la existencia de los demás. Los cambios realizados en un objeto gestionado en un contexto de objeto gestionado no se propagan automáticamente a otros contextos de objetos gestionados. ¿Cómo resolvemos este problema?

Hay dos estrategias populares que Apple recomienda, notificaciones y contextos de objetos gestionados padre-hijo. Miremos cada estrategia e investiguemos sus pros y contras.

El escenario que tomaremos como ejemplo es una subclase de NSOperation que realiza el trabajo en segundo plano y accede a los datos principales en la cadena de fondo de la operación. Este ejemplo le mostrará las diferencias y ventajas de cada estrategia.

Estrategia 1: Notificaciones

Al principio de esta serie, te presenté a la clase NSFetchedResultsController y aprendiste que un contexto de objeto gestionado publica tres tipos de notificaciones:

  • NSManagedObjectContextObjectsDidChangeNotification: esta notificación se publica cuando uno de los objetos gestionados del contexto del objeto gestionado ha cambiado
  • NSManagedObjectContextWillSaveNotification: esta notificación se publica antes de que el contexto del objeto gestionado realice una operación de guardado
  • NSManagedObjectContextDidSaveNotification: esta notificación se publica después de que el contexto del objeto gestionado realiza una operación de guardado

Cuando un contexto de objeto administrado guarda sus cambios en una tienda persistente, a través del persistent store coordinator, otros contextos de objetos administrados pueden querer saber sobre esos cambios. Esto es muy fácil de hacer e incluso es más fácil incluir o fusionar los cambios en otro contexto de objeto gestionado. Hablemos del código.

Creamos una operación no simultánea que hace un poco de trabajo en segundo plano y necesita acceso a los datos principales.

La interfaz de la operación es muy simple, ya que solo contiene una propiedad para el principal contexto de objeto gestionado de la aplicación. Existen varios motivos para mantener una referencia al contexto principal de objetos gestionados de la aplicación. Esto queda claro cuando inspeccionamos la implementación de la clase TSPImportOperation.

Primero declaramos una propiedad privada, privateManagedObjectContext, de tipo NSManagedObjectContext. Este es el contexto del objeto gestionado que la operación usará internamente para realizar tareas de Core Data.

Debido a que estamos implementando una subclase de NSOperation no simultánea, debemos implementar el método main. Esto es lo que parece.

Hay algunos detalles importantes que deben aclararse. Inicializamos el contexto de objeto gestionado privado y establecemos su propiedad de persistent store coordinator utilizando el objeto mainManagedObjectContext. Esto está perfectamente bien, porque no accedemos al mainManagedObjectContext, solo le pedimos su referencia al persistent store coordinator de la aplicación. No violamos la regla de confinamiento del hilo.

Es esencial inicializar el contexto del objeto privado gestionado en el método main de la operación, porque este método se ejecuta en el hilo de fondo en el que se ejecuta la operación. ¿No podemos inicializar el contexto del objeto gestionado en el método init de la operación? La respuesta es no. El método init de la operación se ejecuta en el subproceso en el que se inicializa TSPImportOperation, que probablemente sea el hilo principal. Esto vencería el propósito de un contexto privado de objetos gestionados.

En el método principal de la operación, agregamos la instancia TSPImportOperation como un observador de cualquier notificación NSManagedObjectContextDidSaveNotification publicada por el contexto de objeto privado administrado.

A continuación, hacemos el trabajo para el que se creó la operación y guardamos los cambios del contexto del objeto gestionado privado, que activará una notificación NSManagedObjectContextDidSaveNotification. Echemos un vistazo a lo que sucede en el método managedObjectContextDidSave:

Como puede ver, su implementación es corta y simple. Llamamos a mergeChangesFromContextDidSaveNotification: en el contexto principal del objeto gestionado, pasando el objeto de notificación. Como mencioné anteriormente, la notificación contiene los cambios, inserciones, actualizaciones y eliminaciones del contexto del objeto privado administrado. Es clave llamar a este método en el subproceso en el que se creó el contexto principal del objeto gestionado, el hilo principal. Es por eso que enviamos esta llamada al hilo principal.

Poner en uso la clase TSPImportOperation es tan simple como inicializar una instancia, establecer su propiedad mainManagedObjectContext y agregar la operación a una cola de operaciones.

Estrategia 2: Contextos de objetos administrados padre / hijo

Desde iOS 6, hay una estrategia aún mejor y más elegante. Revisemos la clase TSPImportOperation y aprovechemos los contextos de objetos gestionados padre / hijo. El concepto detrás de los contextos de objetos gestionados padre / hijo es simple pero poderoso. Déjame explicarte cómo funciona.

Un contexto de objeto gestionado hijo depende de su contexto de objeto gestionado primario para guardar sus cambios en el almacén persistente correspondiente. De hecho, un contexto de objeto administrado hijo no tiene acceso a un persistent store coordinator. Siempre que se guarde un contexto de objeto gestionado hijo, los cambios que contiene se envían al contexto de objeto gestionado principal. No es necesario utilizar notificaciones para fusionar manualmente los cambios en el contexto principal o principal del objeto gestionado.

Otro beneficio es el rendimiento. Debido a que el contexto de objeto gestionado hijo no tiene acceso al coordinador de tienda persistente, los cambios no se envían a este último cuando se guarda el contexto de objeto gestionado hijo. En cambio, los cambios se llevan al contexto del objeto principal administrado, ensuciándolo. Los cambios no se propagan automáticamente al coordinador de tienda persistente.

Los contextos de objetos administrados se pueden anidar. Un contexto de objeto gestionado hijo puede tener un contexto propio de objeto gestionado hijo. Se aplican las mismas reglas. Sin embargo, es importante recordar que los cambios que se envían al contexto del objeto administrado principal no se reducen a ningún otro contexto de objetos administrados secundarios. Si el niño A empujó sus cambios a su padre, entonces el niño B no tiene conocimiento de estos cambios.

La creación de un contexto de objetos gestionados por niños es ligeramente diferente de lo que hemos visto hasta ahora. Un contexto de objeto gestionado hijo utiliza un inicializador diferente, initWithConcurrencyType:. El tipo de simultaneidad que acepta el inicializador define el modelo de subprocesamiento del contexto del objeto gestionado. Veamos cada tipo de concurrencia.

  • NSMainQueueConcurrencyType: el contexto del objeto gestionado solo es accesible desde el hilo principal. Se lanza una excepción si intenta acceder desde cualquier otro hilo.
  • NSPrivateQueueConcurrencyType: al crear un contexto de objeto gestionado con un tipo de concurrencia de NSPrivateQueueConcurrencyType, el contexto del objeto gestionado está asociado a una cola privada y solo se puede acceder desde esa cola privada.
  • NSConfinementConcurrencyType: este es el tipo de simultaneidad que se corresponde con el concepto de confinamiento de procesos que exploramos anteriormente. Si crea un contexto de objeto gestionado utilizando el método init, el tipo de concurrencia de ese contexto de objeto gestionado es NSConfinementConcurrencyType.

Hay dos métodos clave que se agregaron al marco de Core Data cuando Apple introdujo contextos de objetos gestionados padre / hijo, performBlock: y performBlockAndWait:. Ambos métodos harán tu vida mucho más fácil. Cuando llama a performBlock: en un contexto de objeto gestionado y pasa un bloque de código para ejecutar, Core Data se asegura de que el bloque se ejecuta en el hilo correcto. En el caso del tipo de concurrencia NSPrivateQueueConcurrencyType, esto significa que el bloque se ejecuta en la cola privada de ese contexto de objeto gestionado.

La diferencia entre performBlock: y performBlockAndWait: es simple. El método performBlock: no bloquea el hilo actual. Acepta el bloque, lo programa para la ejecución en la cola correcta y continúa con la ejecución de la siguiente declaración.

El método performBlockAndWait: sin embargo, está bloqueando. El hilo desde el que se realiza performBlockAndWait: espera a que el bloque que se pasa al método finalice antes de ejecutar la siguiente instrucción. La ventaja es que las llamadas anidadas para performBlockAndWait: se ejecutan en orden.

Para finalizar este artículo, me gustaría refactorizar la clase TSPImportOperation para aprovechar los contextos de objetos gestionados padre / hijo. Pronto notará que simplifica en gran medida la subclase TSPImportOperation.

El encabezado permanece sin cambios, pero el método main cambia bastante. Eche un vistazo a su implementación actualizada a continuación.

Eso es. El contexto principal del objeto gestionado es el elemento primario del contexto del objeto gestionado privado. Tenga en cuenta que no establecemos la propiedad persistentStoreCoordinator del contexto del objeto privado administrado y no agregamos la operación como un observador para notificaciones NSManagedObjectContextDidSaveNotification. Cuando se guarda el contexto del objeto privado administrado, los cambios se envían automáticamente a su contexto de objeto principal administrado. Core Data garantiza que esto ocurra en el hilo correcto. Depende del contexto del objeto gestionado principal, el contexto del objeto gestionado principal, enviar los cambios al persistent store coordinator.

Conclusión

La simultaneidad no es fácil de comprender o implementar, pero es ingenuo pensar que nunca se encontrará con una situación en la que necesite realizar operaciones de Core Data en un proceso de fondo.

En los próximos dos artículos, te contaré sobre iOS 8 y Core Data. Apple introdujo una serie de nuevas API en iOS 8 y OS X 10.10, incluidas la actualización por lotes y la recuperación asincrónica.

Advertisement
Advertisement
Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.