En informática e ingeniería , la memoria transaccional intenta simplificar la programación concurrente al permitir que un grupo de instrucciones de carga y almacenamiento se ejecuten de manera 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 una 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 concurrentes de datos compartidos en sistemas paralelos. [1]
En la programación concurrente, la sincronización es necesaria cuando los 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 poco conflicto 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 al aplicar atomicidad , consistencia y aislamiento .
Una transacción es una colección de operaciones que pueden ejecutarse 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 en su lugar, 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 pasar por una transacción y producir un resultado serializable . Por ejemplo, el código se puede escribir como:
def transfer_money ( desde_cuenta , a_cuenta , monto ): """Transferir dinero de una cuenta a otra.""" with transaction (): desde_cuenta . balance -= monto a_cuenta . balance += monto
En el código, el bloque definido por "transacción" tiene garantizada la atomicidad, la consistencia y el aislamiento de 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 realice ninguna acción. 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 un 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 es limitada, ya que requiere una abstracción de memoria compartida. Aunque los programas de memoria transaccional no pueden producir un bloqueo, los programas pueden sufrir un bloqueo activo o una 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 memoria caché y el protocolo de bus para soportar transacciones. [4] [5] [6] [7] [8] Los valores especulativos en una transacción deben almacenarse en búfer y permanecer ocultos por otros subprocesos hasta el momento de confirmación. Se utilizan búferes grandes para almacenar valores especulativos mientras se evita la propagación de escritura a través del protocolo de coherencia de caché subyacente . Tradicionalmente, los búferes se han implementado utilizando diferentes estructuras dentro de la jerarquía de memoria, como colas de almacenamiento o cachés. Los búferes más alejados del procesador, como el caché L2, pueden contener más valores especulativos (hasta unos pocos megabytes). El tamaño óptimo de un búfer aún está en debate debido al uso limitado de transacciones en programas comerciales. [3] En una implementación de caché, las líneas de caché generalmente se amplían con bits de lectura y escritura. Cuando el controlador de hardware recibe una solicitud, el controlador usa estos bits para detectar un conflicto. Si se detecta un conflicto de serialización en una transacción paralela, se descartan los valores especulativos. Cuando se utilizan cachés, el sistema puede introducir el riesgo de conflictos falsos debido al uso de la granularidad de la línea de caché. [3] La memoria condicional de carga, enlace y almacenamiento (LL/SC) que ofrecen muchos procesadores RISC puede considerarse como el soporte de memoria transaccional más básico; sin embargo, LL/SC generalmente opera con datos que tienen el 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 un rendimiento máximo en comparación con las alternativas de software, se ha visto un uso limitado en este momento.
La memoria transaccional de software proporciona semántica de memoria transaccional en una biblioteca de ejecución de software o en el lenguaje de programación [9] y requiere un soporte de hardware mínimo (normalmente una operación de intercambio y comparación atómica , o equivalente). Como desventaja, las implementaciones de software suelen tener una penalización de rendimiento, en comparación con las soluciones de hardware. La aceleración de hardware puede reducir algunos de los costos generales asociados con la memoria transaccional de software.
Debido a la naturaleza más limitada de la memoria transaccional de hardware (en las implementaciones actuales), el software que la utiliza puede requerir un ajuste bastante extenso para aprovecharla al máximo. 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 problemas de alineación de caché y compartición falsa); en el contexto de una máquina virtual, varios subprocesos en segundo plano pueden causar abortos inesperados de transacciones. [10]
Una de las primeras implementaciones de la memoria transaccional fue el buffer de almacenamiento controlado utilizado en los procesadores Crusoe y Efficeon de Transmeta . Sin embargo, esto solo se utilizó para facilitar optimizaciones especulativas para la traducción binaria, en lugar de cualquier forma de multiprocesamiento 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 extraños. [11]
Sun Microsystems implementó la memoria transaccional de hardware y una forma limitada de subprocesamiento múltiple especulativo en su procesador Rock de alta gama . Esta implementación demostró que podía usarse para la elisión de bloqueos y sistemas de memoria transaccional híbridos 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, varios sistemas prototipo estaban disponibles para los investigadores. [11]
En 2009, AMD propuso la Advanced Synchronization Facility (ASF), un conjunto de extensiones x86 que proporcionan una forma muy limitada de compatibilidad con la memoria transaccional de hardware. El objetivo era proporcionar primitivas de hardware que pudieran utilizarse para una sincronización de nivel superior, como la memoria transaccional de software o algoritmos sin bloqueos. Sin embargo, AMD no ha anunciado si se utilizará ASF en 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 multihilo especulativo. La memoria transaccional se podía configurar en dos modos; el primero es un modo desordenado y de una sola versión, 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 multihilo especulativo, proporcionando una memoria transaccional ordenada y con múltiples versiones. Los hilos especulativos pueden tener diferentes versiones de la misma dirección de memoria, y la implementación de hardware realiza un seguimiento de la edad de cada hilo. Los hilos más jóvenes pueden acceder a datos de hilos más antiguos (pero no al revés), y las escrituras en la misma dirección se basan en el orden de los hilos. En algunos casos, las dependencias entre hilos pueden hacer que las versiones más jóvenes se interrumpan. [11]
Las extensiones de sincronización transaccional (TSX) de Intel están disponibles en algunos procesadores Skylake . Anteriormente también se habían implementado en procesadores Haswell y Broadwell , pero ambas implementaciones resultaron defectuosas y se deshabilitó el soporte para TSX. La especificación TSX describe la API de memoria transaccional para que la utilicen los desarrolladores de software, pero no proporciona 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 incorpora memoria transaccional al lenguaje.