En informática e ingeniería , la memoria transaccional intenta simplificar la programación concurrente permitiendo que un grupo de instrucciones de carga y almacenamiento se ejecuten de forma atómica . Es un mecanismo de control de concurrencia análogo a las transacciones de bases de datos para controlar el acceso a la memoria compartida en computación concurrente . Los sistemas de memoria transaccional proporcionan abstracción de alto nivel como alternativa a la sincronización de subprocesos de bajo nivel. Esta abstracción permite la coordinación entre lecturas y escrituras simultáneas de datos compartidos en sistemas paralelos. [1]
En la programación concurrente, se requiere sincronización cuando subprocesos paralelos intentan acceder a un recurso compartido. Las construcciones de sincronización de subprocesos de bajo nivel, como los bloqueos, son pesimistas y prohíben que los subprocesos que están fuera de una sección crítica ejecuten el código protegido por la sección crítica. El proceso de aplicar y liberar bloqueos a menudo funciona como una sobrecarga adicional en cargas de trabajo con pocos conflictos entre subprocesos. La memoria transaccional proporciona un control de concurrencia optimista al permitir que los subprocesos se ejecuten en paralelo con una interferencia mínima. [2] El objetivo de los sistemas de memoria transaccional es admitir de forma transparente regiones de código marcadas como transacciones imponiendo atomicidad , coherencia y aislamiento .
Una transacción es una colección de operaciones que pueden ejecutar y confirmar cambios siempre que no exista un conflicto. Cuando se detecta un conflicto, una transacción volverá a su estado inicial (antes de cualquier cambio) y se volverá a ejecutar hasta que se eliminen todos los conflictos. Antes de una confirmación exitosa, el resultado de cualquier operación es puramente especulativo dentro de una transacción. A diferencia de la sincronización basada en bloqueos, donde las operaciones se serializan para evitar la corrupción de datos, las transacciones permiten un paralelismo adicional siempre que pocas operaciones intenten modificar un recurso compartido. Dado que el programador no es responsable de identificar explícitamente los bloqueos o el orden en que se adquieren, los programas que utilizan memoria transaccional no pueden producir un punto muerto . [2]
Con estas construcciones implementadas, la memoria transaccional proporciona una abstracción de programación de alto nivel al permitir a los programadores encerrar sus métodos dentro de bloques transaccionales. Las implementaciones correctas garantizan que los datos no se puedan compartir entre subprocesos sin realizar una transacción y producir un resultado serializable . Por ejemplo, el código se puede escribir como:
def transfer_money ( from_account , to_account , monto ): """Transferir dinero de una cuenta a otra.""" con transacción (): from_account . saldo -= monto a_cuenta . saldo += importe
En el código, el bloque definido por "transacción" tiene garantizada la atomicidad, la coherencia y el aislamiento mediante la implementación de la memoria transaccional subyacente y es transparente para el programador. Las variables dentro de la transacción están protegidas de conflictos externos, lo que garantiza que se transfiera la cantidad correcta o que no se tome ninguna medida. Tenga en cuenta que los errores relacionados con la concurrencia aún son posibles en programas que utilizan una gran cantidad de transacciones, especialmente en implementaciones de software donde la biblioteca proporcionada por el lenguaje no puede imponer el uso correcto. Los errores introducidos a través de transacciones a menudo pueden ser difíciles de depurar ya que no se pueden colocar puntos de interrupción dentro de una transacción. [2]
La memoria transaccional está limitada porque requiere una abstracción de memoria compartida. Aunque los programas de memoria transaccional no pueden producir un punto muerto, los programas aún pueden sufrir un bloqueo activo o falta de recursos . Por ejemplo, las transacciones más largas pueden revertirse repetidamente en respuesta a múltiples transacciones más pequeñas, lo que desperdicia tiempo y energía. [2]
La abstracción de la atomicidad en la memoria transaccional requiere un mecanismo de hardware para detectar conflictos y deshacer cualquier cambio realizado en los datos compartidos. [3] Los sistemas de memoria transaccional de hardware pueden comprender modificaciones en los procesadores, la caché y el protocolo de bus para soportar transacciones. [4] [5] [6] [7] [8] Los valores especulativos en una transacción deben almacenarse en un buffer y permanecer invisibles para otros subprocesos hasta el momento de la confirmación. Se utilizan búferes grandes para almacenar valores especulativos y al mismo tiempo evitar la propagación de escritura a través del protocolo de coherencia de caché subyacente . Tradicionalmente, los buffers se han implementado utilizando diferentes estructuras dentro de la jerarquía de memoria, como colas de almacenamiento o cachés. Los buffers más alejados del procesador, como el caché L2, pueden contener valores más especulativos (hasta unos pocos megabytes). El tamaño óptimo de un buffer aún está bajo debate debido al uso limitado de transacciones en programas comerciales. [3] En una implementación de caché, las líneas de caché generalmente se aumentan con bits de lectura y escritura. Cuando el controlador de hardware recibe una solicitud, utiliza estos bits para detectar un conflicto. Si se detecta un conflicto de serialización en una transacción paralela, los valores especulativos se descartan. Cuando se utilizan cachés, el sistema puede introducir el riesgo de conflictos falsos debido al uso de granularidad de línea de caché. [3] El enlace de carga/almacenamiento condicional (LL/SC) que ofrecen muchos procesadores RISC puede verse como el soporte de memoria transaccional más básico; sin embargo, LL/SC normalmente opera con datos del tamaño de una palabra de máquina nativa, por lo que solo se admiten transacciones de una sola palabra. [4] Aunque la memoria transaccional de hardware proporciona el máximo rendimiento en comparación con las alternativas de software, en este momento se ha observado un uso limitado.
La memoria transaccional de software proporciona semántica de memoria transaccional en una biblioteca de tiempo de ejecución de software o en el lenguaje de programación, [9] y requiere un soporte mínimo de hardware (normalmente una operación atómica de comparación e intercambio , o equivalente). Como desventaja, las implementaciones de software suelen tener una penalización en el rendimiento, en comparación con las soluciones de hardware. La aceleración de hardware puede reducir algunos de los gastos generales asociados con la memoria transaccional de software.
Debido a la naturaleza más limitada de la memoria transaccional del hardware (en las implementaciones actuales), el software que la utiliza puede requerir ajustes bastante extensos para beneficiarse plenamente de ella. Por ejemplo, el asignador de memoria dinámica puede tener una influencia significativa en el rendimiento y, de la misma manera, el relleno de la estructura puede afectar el rendimiento (debido a la alineación de la caché y a problemas de uso compartido falso); En el contexto de una máquina virtual, varios subprocesos en segundo plano pueden provocar transacciones abortadas inesperadas. [10]
Una de las primeras implementaciones de memoria transaccional fue el búfer de almacenamiento cerrado utilizado en los procesadores Crusoe y Efficeon de Transmeta . Sin embargo, esto solo se usó para facilitar optimizaciones especulativas para la traducción binaria, en lugar de cualquier forma de multiproceso especulativo o exponerlo directamente a los programadores. Azul Systems también implementó memoria transaccional de hardware para acelerar sus dispositivos Java , pero esto también se ocultó a los externos. [11]
Sun Microsystems implementó memoria transaccional de hardware y una forma limitada de subprocesos múltiples especulativos en su procesador Rock de alta gama . Esta implementación demostró que podría usarse para elisión de bloqueos y sistemas de memoria transaccional híbrida más complejos, donde las transacciones se manejan con una combinación de hardware y software. El procesador Rock fue cancelado en 2009, justo antes de la adquisición por parte de Oracle ; Si bien los productos reales nunca se lanzaron al mercado, los investigadores tuvieron a su disposición varios sistemas prototipo. [11]
En 2009, AMD propuso Advanced Synchronization Facility (ASF), un conjunto de extensiones x86 que proporcionan una forma muy limitada de soporte de memoria transaccional de hardware. El objetivo era proporcionar primitivas de hardware que pudieran usarse para sincronización de nivel superior, como memoria transaccional de software o algoritmos sin bloqueo. Sin embargo, AMD no ha anunciado si se utilizará ASF en sus productos y, de ser así, en qué plazo. [11]
Más recientemente, IBM anunció en 2011 que Blue Gene/Q tenía soporte de hardware tanto para memoria transaccional como para subprocesos múltiples especulativos. La memoria transaccional se podría configurar en dos modos; el primero es un modo desordenado y de versión única, donde una escritura de una transacción causa un conflicto con cualquier transacción que lea la misma dirección de memoria. El segundo modo es para subprocesos múltiples especulativos, proporcionando una memoria transaccional ordenada y con múltiples versiones. Los subprocesos especulativos pueden tener diferentes versiones de la misma dirección de memoria y la implementación de hardware realiza un seguimiento de la antigüedad de cada subproceso. Los subprocesos más jóvenes pueden acceder a datos de subprocesos más antiguos (pero no al revés) y las escrituras en la misma dirección se basan en el orden de los subprocesos. En algunos casos, las dependencias entre subprocesos pueden provocar que las versiones más recientes aborten. [11]
Las Extensiones de sincronización transaccional (TSX) de Intel están disponibles en algunos de los procesadores Skylake . También se implementó anteriormente en los procesadores Haswell y Broadwell , pero las implementaciones resultaron defectuosas en ambas ocasiones y la compatibilidad con TSX se deshabilitó. La especificación TSX describe la API de memoria transaccional para uso de los desarrolladores de software, pero oculta detalles sobre la implementación técnica. [11] La arquitectura ARM tiene una extensión similar. [12]
A partir de GCC 4.7, está disponible una biblioteca experimental para memoria transaccional que utiliza una implementación híbrida. La variante PyPy de Python también introduce memoria transaccional al lenguaje.