Para crear aplicaciones de alto rendimiento, los desarrolladores deben escribir código multihilo seguro y fácil de mantener que evite problemas como interbloqueos y condiciones de carrera. Realm ofrece herramientas diseñadas específicamente para aplicaciones multihilo de alto rendimiento.
Tres reglas a seguir
Antes de explorar las herramientas de Realm para aplicaciones multiproceso, debes comprender y seguir estas tres reglas:
- No bloquear para leer:
- Del reino La arquitectura de Control de Concurrencia Multiversión (MVCC) elimina la necesidad de bloqueos para las operaciones de lectura. Los valores leídos nunca se corromperán ni se modificarán parcialmente. Se puede leer libremente desde el mismo archivo Realm en cualquier hilo sin necesidad de bloqueos ni mutex. Un bloqueo innecesario supondría un cuello de botella en el rendimiento, ya que cada hilo podría tener que esperar su turno antes de leer.
- Evite escrituras sincrónicas en el hilo de UI si escribe en un hilo en segundo plano:
- Puedes escribir en un archivo Realm desde cualquier hilo, pero solo puede haber un escritor a la vez. Por lo tanto, las transacciones de escritura síncrona se bloquean entre sí. Una escritura síncrona en el hilo de la interfaz de usuario puede provocar que tu aplicación parezca no responder mientras espera a que se complete una escritura en un hilo en segundo plano. Device Sync escribe en un hilo en segundo plano, por lo que debes evitar las escrituras síncronas en el hilo de la interfaz de usuario con reinos sincronizados.
- No pase objetos activos, colecciones o reinos a otros subprocesos:
- Los objetos activos, las colecciones y las instancias de realm están confinados a subprocesos: es decir, solo son válidos en el subproceso en el que se crearon. En la práctica, esto significa que no se pueden pasar instancias activas a otros subprocesos. Sin embargo, Realm ofrece varios mecanismos para compartir objetos entre subprocesos.
Comunicación a través de subprocesos
Para acceder al mismo archivo de dominio desde diferentes subprocesos, debe crear una instancia de dominio en cada subproceso que necesite acceso. Siempre que especifique la misma configuración, todas las instancias de dominio se asignarán al mismo archivo en el disco.
Una de las reglas clave al trabajar con Realm en un entorno multihilo es que los objetos están confinadosa cada hilo: no se puede acceder a las instancias de un reino, colección u objeto originadas en otros hilos. La arquitectura de Control de Concurrencia Multiversión (MVCC) de Realm implica que podría haber varias versiones activas de un objeto en cualquier momento. El confinamiento de hilos garantiza que todas las instancias de ese hilo tengan la misma versión interna.
Cuando necesita comunicarse a través de subprocesos, tiene varias opciones según su caso de uso:
Para modificar un objeto en dos subprocesos, consulte el objeto en ambos subprocesos.
Para reaccionar a los cambios realizados en cualquier hilo, utilice las notificaciones de Realm.
Para ver los cambios que ocurrieron en otro hilo en la instancia de reino del hiloactual, actualice su instancia de reino.
Para compartir una instancia de un reino o un objeto específico con otro hilo, comparta una thread_safe_reference con la instancia del reino o el objeto.
Para enviar una vista rápida y de solo lectura del objeto a otros subprocesos, "congele" el objeto.
Pasar instancias a través de subprocesos
Instancias de realm::realm, realm::results y realm::object están restringidos al subproceso. Esto significa que solo puedes usarlos en el subproceso donde los creaste.
Puede copiar instancias confinadas a un subproceso a otro subproceso de la siguiente manera:
Inicialice una thread_safe_reference con el objeto confinado al hilo.
Pase la referencia al hilo de destino.
Resuelve la referencia en el hilo de destino. Si el objeto al que se hace referencia es una instancia Realm, resuélvalo llamando a
.resolve(); de lo contrario, traslada la referencia arealm.resolve(). El objeto devuelto ahora está confinado a la hebra objetivo, como si se hubiera creado en la hebra de destino en lugar de en la hebra original.
Importante
Debe resolver un thread_safe_reference solo una vez. De lo contrario, el dominio de origen permanecerá anclado hasta que se desasigne la referencia. Por esta razón, thread_safe_reference debería tener una duración corta.
// Put a managed object into a thread safe reference auto threadSafeItem = realm::thread_safe_reference<realm::Item>{managedItem}; // Move the thread safe reference to a background thread auto thread = std::thread([threadSafeItem = std::move(threadSafeItem), path]() mutable { // Open the database again on the background thread auto backgroundConfig = realm::db_config(); backgroundConfig.set_path(path); auto backgroundRealm = realm::db(std::move(backgroundConfig)); // Resolve the Item instance via the thread safe // reference auto item = backgroundRealm.resolve(std::move(threadSafeItem)); // ... use item ... }); // Wait for thread to complete thread.join();
Otra forma de trabajar con un objeto en otro hilo es consultarlo de nuevo en ese hilo. Sin embargo, si el objeto no tiene una clave principal, consultarlo no es trivial. Puedes usar thread_safe_reference en cualquier objeto, independientemente de si tiene una clave principal.
Utilice el mismo reino en todos los subprocesos
No es posible compartir instancias de reino entre subprocesos.
Para usar el mismo archivo de Realm en varios subprocesos, abra una instancia de Realm diferente en cada subproceso. Siempre que use la misma configuración, todas las instancias de Realm se asignarán al mismo archivo en el disco.
Transferir copias inmutables entre subprocesos
Los objetos activos y confinados en subprocesos funcionan correctamente en la mayoría de los casos. Sin embargo, algunas aplicaciones, como las basadas en arquitecturas reactivas basadas en flujos de eventos, necesitan enviar copias inmutables a varios subprocesos para su procesamiento antes de llegar al subproceso de la interfaz de usuario. Realizar una copia profunda cada vez sería costoso, y Realm no permite compartir instancias activas entre subprocesos. En este caso, se pueden congelar y descongelar objetos, colecciones y reinos.
La congelación crea una vista inmutable de un objeto, colección o reino específico. El objeto, colección o reino congelado sigue existiendo en el disco y no necesita copiarse completamente al pasarse a otros hilos. Puedes compartir libremente el objeto congelado entre hilos sin preocuparte por problemas. Al congelar un reino, sus objetos secundarios también se congelan.
Los objetos congelados no están activos y no se actualizan automáticamente. Son, en realidad, instantáneas del estado del objeto en el momento de la congelación. Al descongelar un objeto, se devuelve una versión activa del objeto congelado.
Para congelar un reino, una colección o un objeto, llame al método .freeze():
auto realm = realm::db(std::move(config)); // Get an immutable copy of the database that can be passed across threads. auto frozenRealm = realm.freeze(); if (frozenRealm.is_frozen()) { // Do something with the frozen database. // You may pass a frozen realm, collection, or objects // across threads. Or you may need to `.thaw()` // to make it mutable again. } // You can freeze collections. auto managedItems = realm.objects<realm::Item>(); auto frozenItems = managedItems.freeze(); CHECK(frozenItems.is_frozen()); // You can read from frozen databases. auto itemsFromFrozenRealm = frozenRealm.objects<realm::Item>(); CHECK(itemsFromFrozenRealm.is_frozen()); // You can freeze objects. auto managedItem = managedItems[0]; auto frozenItem = managedItem.freeze(); CHECK(frozenItem.is_frozen()); // Frozen objects have a reference to a frozen realm. CHECK(frozenItem.get_realm().is_frozen());
Al trabajar con objetos congelados, cualquier intento de realizar cualquiera de las siguientes acciones genera una excepción:
Abriendo una transacción de escritura en un reino congelado.
Modificar un objeto congelado.
Agregar un detector de cambios a un reino, una colección o un objeto congelado.
Puedes usar .is_frozen() para comprobar si el objeto está congelado. Esto siempre es seguro para subprocesos.
if (frozenRealm.is_frozen()) { // Do something with the frozen database. // You may pass a frozen realm, collection, or objects // across threads. Or you may need to `.thaw()` // to make it mutable again. }
Los objetos congelados siguen siendo válidos mientras el reino activo que los generó permanezca abierto. Por lo tanto, evite cerrar el reino activo hasta que todos los hilos hayan terminado con los objetos congelados. Puede cerrar un reino congelado antes de que se cierre el reino activo.
Importante
Sobre el almacenamiento en caché de objetos congelados
Almacenar en caché demasiados objetos congelados puede tener un impacto negativo en el tamaño del archivo Realm. "Demasiados" depende de tu dispositivo objetivo específico y el tamaño de tus objetos Realm. Si necesitas almacenar en caché un gran número de versiones, considera copiar lo que necesitas fuera de realm.
Modificar un objeto congelado
Para modificar un objeto congelado, debe descongelarlo. Como alternativa, puede consultarlo en un dominio no congelado y luego modificarlo. Llamar a .thaw() en un objeto, colección o dominio activo lo devuelve.
Descongelar un objeto o una colección también descongela el reino al que hace referencia.
// Read from a frozen database. auto frozenItems = frozenRealm.objects<realm::Item>(); // The collection that we pull from the frozen database is also frozen. CHECK(frozenItems.is_frozen()); // Get an individual item from the collection. auto frozenItem = frozenItems[0]; // To modify the item, you must first thaw it. // You can also thaw collections and realms. auto thawedItem = frozenItem.thaw(); // Check to make sure the item is valid. An object is // invalidated when it is deleted from its managing database, // or when its managing realm has invalidate() called on it. REQUIRE(thawedItem.is_invalidated() == false); // Thawing the item also thaws the frozen database it references. auto thawedRealm = thawedItem.get_realm(); REQUIRE(thawedRealm.is_frozen() == false); // With both the object and its managing database thawed, you // can safely modify the object. thawedRealm.write([&] { thawedItem.name = "Save the world"; });
Añadir a una colección congelada
Al agregar algo a una colección congelada, debe descongelar tanto el objeto que contiene la colección como el objeto que desea agregar.
La misma regla se aplica al pasar objetos congelados entre subprocesos. Un caso común podría ser llamar a una función en un subproceso en segundo plano para realizar algún trabajo en lugar de bloquear la interfaz de usuario.
En este ejemplo, consultamos dos objetos en un reino congelado:
Un objeto
Projectque tiene una propiedad de lista deItemobjetosUn objeto
Item
Se deben descongelar ambos objetos antes de poder agregar el Item a la colección de listas items en el/la Project. Si descongelamos solo el objeto Project pero no el Item, Realm arroja un error.
// Get frozen objects. // Here, we're getting them from a frozen database, // but you might also be passing them across threads. auto frozenItems = frozenRealm.objects<realm::Item>(); // The collection that we pull from the frozen database is also frozen. CHECK(frozenItems.is_frozen()); // Get the individual objects we want to work with. auto specificFrozenItems = frozenItems.where( [](auto const& item) { return item.name == "Save the cheerleader"; }); auto frozenProjects = frozenRealm.objects<realm::Project>().where( [](auto const& project) { return project.name == "Heroes: Genesis"; }); ; auto frozenItem = specificFrozenItems[0]; auto frozenProject = frozenProjects[0]; // Thaw the frozen objects. You must thaw both the object // you want to append and the object whose collection // property you want to append to. auto thawedItem = frozenItem.thaw(); auto thawedProject = frozenProject.thaw(); auto managingRealm = thawedProject.get_realm(); managingRealm.write([&] { thawedProject.items.push_back(thawedItem); });
struct Item { std::string name; }; REALM_SCHEMA(Item, name)
struct Project { std::string name; std::vector<realm::Item*> items; }; REALM_SCHEMA(Project, name, items)
Programadores (bucles de ejecución)
Algunas plataformas o frameworks configuran automáticamente un programador (o bucle de ejecución), que procesa eventos continuamente durante el ciclo de vida de la aplicación. El SDK de Realm para C++ detecta y utiliza programadores en las siguientes plataformas o frameworks:
macOS, iOS, tvOS, watchOS
Android
Qt
Realm utiliza el programador para programar trabajos como la carga y descarga de sincronización de dispositivos.
Si su plataforma no tiene un programador compatible o si desea usar uno personalizado, puede implementar realm::scheduler y pasar la instancia al realm::db_config que usa para configurar el dominio. Realm usará el programador que le pase.
struct MyScheduler : realm::scheduler { MyScheduler() { // ... Kick off task processor thread(s) and run until the scheduler // goes out of scope ... } ~MyScheduler() override { // ... Call in the processor thread(s) and block until return ... } void invoke(std::function<void()>&& task) override { // ... Add the task to the (lock-free) processor queue ... } [[nodiscard]] bool is_on_thread() const noexcept override { // ... Return true if the caller is on the same thread as a processor // thread ... } bool is_same_as(const realm::scheduler* other) const noexcept override { // ... Compare scheduler instances ... } [[nodiscard]] bool can_invoke() const noexcept override { // ... Return true if the scheduler can accept tasks ... } // ... }; int main() { // Set up a custom scheduler. auto scheduler = std::make_shared<MyScheduler>(); // Pass the scheduler instance to the realm configuration. auto config = realm::db_config{path, scheduler}; // Start the program main loop. auto done = false; while (!done) { // This assumes the scheduler is implemented so that it // continues processing tasks on background threads until // the scheduler goes out of scope. // Handle input here. // ... if (shouldQuitProgram) { done = true; } } }
Reinos refrescantes
En cualquier hilo controlado por un programador o un bucle de ejecución, Realm actualiza automáticamente los objetos al inicio de cada iteración del bucle. Entre iteraciones del bucle, trabajará en la instantánea, por lo que cada método siempre tendrá una vista consistente y no tendrá que preocuparse por lo que sucede en otros hilos.
Al abrir un dominio en un hilo, su estado será la última confirmación de escritura correcta y permanecerá en esa versión hasta que se actualice. Si un hilo no está controlado por un bucle de ejecución, se debe llamar manualmente al método realm.refresh() para avanzar la transacción al estado más reciente.
realm.refresh();
Nota
Si no se actualizan los reinos regularmente, es posible que algunas versiones de transacciones queden "fijadas", lo que impide que Realm reutilice el espacio en disco utilizado por esa versión y genera tamaños de archivo más grandes.
El modelo de subprocesos de Realm en profundidad
Realm proporciona acceso seguro, rápido, sin bloqueos y simultáneo entre subprocesos con su Control de concurrencia multiversión (MVCC) arquitectura.
Comparado y contrastado con Git
Si está familiarizado con un sistema de control de versiones distribuido como Git, es posible que ya tenga una comprensión intuitiva de MVCC. Dos elementos fundamentales de Git son:
Confirmaciones, que son escrituras atómicas.
Ramas, que son diferentes versiones del historial de confirmaciones.
De forma similar, Realm cuenta con escrituras confirmadas atómicamente en forma de transacciones. Realm también cuenta con múltiples versiones del historial en un momento dado, como las ramas.
A diferencia de Git, que promueve activamente la distribución y la divergencia mediante bifurcaciones, un realm solo tiene una versión más reciente en un momento dado y siempre escribe en la cabecera de esa última versión. Realm no puede escribir en una versión anterior. Esto significa que sus datos convergen en una versión más reciente de la verdad.
Estructura interna
Un reino se implementa mediante una estructura de datos de árbol B+. El nodo de nivel superior representa una versión del reino; los nodos secundarios son objetos de esa versión. El reino tiene un puntero a su última versión, similar a cómo Git tiene un puntero a su confirmación HEAD.
Realm utiliza una técnica de copia en escritura para garantizar el aislamiento y la durabilidad. Al realizar cambios, Realm copia la parte relevante del árbol para su escritura. A continuación, Realm confirma los cambios en dos fases:
Realm escribe los cambios en el disco y verifica el éxito.
Luego, Realm establece su puntero de última versión para apuntar a la versión recién escrita.
Este proceso de confirmación en dos pasos garantiza que, incluso si la escritura falla a mitad de camino, la versión original no se corrompa, ya que los cambios se realizaron en una copia de la parte relevante del árbol. Asimismo, el puntero raíz del dominio apuntará a la versión original hasta que se garantice la validez de la nueva versión.
Ejemplo
El siguiente diagrama ilustra el proceso de confirmación:

El dominio está estructurado como un árbol. Tiene un puntero a su última versión, V1.
Cuando se realiza una escritura, Realm crea una nueva versión V2 basada en V1. Realm crea copias de objetos para su modificación (A 1, C 1), mientras que los enlaces a objetos no modificados siguen apuntando a las versiones originales (B, D).
Después de validar la confirmación, Realm actualiza el puntero a la nueva versión más reciente, V2. A continuación, Realm descarta los nodos antiguos que ya no están conectados al árbol.
Realm utiliza técnicas de copia cero, como el mapeo de memoria, para gestionar los datos. Al leer un valor del realm, prácticamente se ve el valor en el disco real, no una copia. Esta es la base de los objetos activos. Por eso también se puede configurar un puntero principal del realm para que apunte a la nueva versión tras validar la escritura en el disco.