stringtranslate.com

Comparar e intercambiar

En informática , compare-and-swap ( CAS ) es una instrucción atómica utilizada en subprocesos múltiples para lograr la sincronización . Compara el contenido de una ubicación de memoria con un valor dado y, solo si son iguales, modifica el contenido de esa ubicación de memoria a un nuevo valor dado. Esto se hace como una única operación atómica. La atomicidad garantiza que el nuevo valor se calcule en función de información actualizada; si el valor hubiera sido actualizado por otro subproceso mientras tanto, la escritura fallaría. El resultado de la operación debe indicar si realizó la sustitución; esto se puede hacer con una simple respuesta booleana (esta variante a menudo se llama compare-and-set ), o devolviendo el valor leído de la ubicación de memoria ( no el valor escrito en ella).

Descripción general

Una operación de comparación e intercambio es una versión atómica del siguiente pseudocódigo , donde * denota acceso a través de un puntero : [1]

función cas(p: puntero a int, antiguo: int, nuevo: int) es  si *p ≠ antiguo devuelve falso *p ← nuevo devuelve verdadero

Esta operación se utiliza para implementar primitivas de sincronización como semáforos y mutexes , [1] así como algoritmos más sofisticados sin bloqueos y sin esperas . Maurice Herlihy (1991) demostró que CAS puede implementar más de estos algoritmos que lectura atómica , escritura o búsqueda y adición , y asumiendo una cantidad bastante grande [ aclaración necesaria ] de memoria, que puede implementarlos todos. [2] CAS es equivalente a load-link/store-conditional , en el sentido de que se puede usar un número constante de invocaciones de cualquiera de las primitivas para implementar la otra de manera sin esperas . [3]

Los algoritmos creados en torno a CAS suelen leer una ubicación de memoria clave y recordar el valor anterior. Basándose en ese valor anterior, calculan un valor nuevo. A continuación, intentan intercambiar el nuevo valor mediante CAS, donde la comparación comprueba que la ubicación sigue siendo igual al valor anterior. Si CAS indica que el intento ha fallado, debe repetirse desde el principio: se vuelve a leer la ubicación, se vuelve a calcular un nuevo valor y se vuelve a intentar el CAS. En lugar de volver a intentarlo inmediatamente después de que una operación CAS falla, los investigadores han descubierto que el rendimiento total del sistema se puede mejorar en sistemas multiprocesador (donde muchos subprocesos actualizan constantemente alguna variable compartida en particular) si los subprocesos que ven que su CAS falla utilizan un retroceso exponencial ; en otras palabras, esperan un poco antes de volver a intentar el CAS. [4]

Ejemplo de aplicación: sumador atómico

Como ejemplo de caso de uso de comparación e intercambio, aquí hay un algoritmo para incrementar o decrementar atómicamente un entero . Esto es útil en una variedad de aplicaciones que usan contadores. La función add realiza la acción *p ← *p + a , atómicamente (nuevamente denotando la indirección del puntero por * , como en C) y devuelve el valor final almacenado en el contador. A diferencia del pseudocódigo cas anterior, no hay ningún requisito de que cualquier secuencia de operaciones sea atómica excepto cas .

función add(p: puntero a int, a: int) devuelve int hecho ← falso mientras no esté hecho valor ← *p // Incluso esta operación no necesita ser atómica. hecho ← cas(p, valor, valor + a) valor de retorno + a

En este algoritmo, si el valor de *p cambia después (o mientras) se obtiene y antes de que el CAS realice el almacenamiento, el CAS notará e informará este hecho, lo que hará que el algoritmo vuelva a intentarlo. [5]

Problema de ABA

Algunos algoritmos basados ​​en CAS se ven afectados por el problema de una coincidencia de falso positivo o el problema ABA , y deben solucionarlo . Es posible que entre el momento en que se lee el valor anterior y el momento en que se intenta realizar CAS, otros procesadores o subprocesos cambien la ubicación de la memoria dos o más veces de modo que adquiera un patrón de bits que coincida con el valor anterior. El problema surge si este nuevo patrón de bits, que se ve exactamente como el valor anterior, tiene un significado diferente: por ejemplo, podría ser una dirección reciclada o un contador de versión encapsulada.

Una solución general para esto es usar un CAS de doble longitud (DCAS). Por ejemplo, en un sistema de 32 bits, se puede usar un CAS de 64 bits. La segunda mitad se usa para almacenar un contador. La parte de comparación de la operación compara el valor leído previamente del puntero y el contador, con el puntero y el contador actuales. Si coinciden, se produce el intercambio (se escribe el nuevo valor), pero el nuevo valor tiene un contador incrementado. Esto significa que si se ha producido un ABA, aunque el valor del puntero será el mismo, es extremadamente improbable que el contador sea el mismo (para un valor de 32 bits, tendría que haber ocurrido un múltiplo de 2 32 operaciones, lo que haría que el contador se ajustara y, en ese momento, el valor del puntero también tendría que ser, por casualidad, el mismo).

Una forma alternativa de esto (útil en CPU que carecen de DCAS) es utilizar un índice en una lista libre, en lugar de un puntero completo, por ejemplo, con un CAS de 32 bits, utilice un índice de 16 bits y un contador de 16 bits. Sin embargo, las longitudes reducidas de los contadores comienzan a hacer posible el ABA a velocidades de CPU modernas.

Una técnica simple que ayuda a aliviar este problema es almacenar un contador ABA en cada elemento de la estructura de datos, en lugar de utilizar un solo contador ABA para toda la estructura de datos.

Una solución más complicada pero más efectiva es implementar la recuperación de memoria segura (SMR, por sus siglas en inglés). Esto es, en efecto, una recolección de basura sin bloqueos. La ventaja de usar SMR es la seguridad de que un puntero dado existirá solo una vez en cualquier momento en la estructura de datos, por lo que el problema de ABA se resuelve completamente. (Sin SMR, se utilizará algo así como una lista libre, para garantizar que se pueda acceder a todos los elementos de datos de forma segura (sin violaciones de acceso a memoria) incluso cuando ya no estén presentes en la estructura de datos. Con SMR, solo se accederá a los elementos que realmente se encuentran actualmente en la estructura de datos).

Costos y beneficios

En ocasiones se piensa que CAS y otras instrucciones atómicas son innecesarias en sistemas monoprocesador, porque la atomicidad de cualquier secuencia de instrucciones se puede lograr deshabilitando las interrupciones mientras se ejecuta. Sin embargo, deshabilitar las interrupciones tiene numerosas desventajas. Por ejemplo, se debe confiar en que el código al que se le permite hacerlo no sea malicioso y monopolice la CPU, así como en que sea correcto y no cuelgue accidentalmente la máquina en un bucle infinito o en un fallo de página. Además, deshabilitar las interrupciones a menudo se considera demasiado costoso para ser práctico. Por lo tanto, incluso los programas destinados solo a ejecutarse en máquinas monoprocesador se beneficiarán de las instrucciones atómicas, como en el caso de futexes de Linux .

En los sistemas multiprocesador, normalmente es imposible desactivar las interrupciones en todos los procesadores al mismo tiempo. Incluso si fuera posible, dos o más procesadores podrían estar intentando acceder a la memoria del mismo semáforo al mismo tiempo, y por lo tanto no se lograría la atomicidad. La instrucción de comparación e intercambio permite que cualquier procesador pruebe y modifique atómicamente una ubicación de memoria, lo que evita este tipo de colisiones entre múltiples procesadores.

En las arquitecturas multiprocesador de nivel de servidor de la década de 2010, la comparación y el intercambio son económicos en comparación con una carga simple que no se sirve desde la memoria caché. Un artículo de 2013 señala que un CAS es solo 1,15 veces más caro que una carga sin caché en Intel Xeon ( Westmere-EX ) y 1,35 veces en AMD Opteron (Magny-Cours). [6]

Implementaciones

La instrucción de comparación e intercambio (y de comparación e intercambio doble) ha sido parte integral de las arquitecturas IBM 370 (y todas las posteriores) desde 1970. Los sistemas operativos que funcionan en estas arquitecturas hacen un uso extensivo de esta instrucción para facilitar el paralelismo de procesos (es decir, tareas del sistema y del usuario) y procesadores (es decir, procesadores centrales), al tiempo que eliminan, en la mayor medida posible, los " bloqueos de giro desactivados " que se habían empleado en los sistemas operativos IBM anteriores. De manera similar, también se eliminó el uso de prueba y configuración . En estos sistemas operativos, las nuevas unidades de trabajo pueden instanciarse "globalmente", en la lista de prioridades de servicios globales, o "localmente", en la lista de prioridades de servicios locales, mediante la ejecución de una única instrucción de comparación e intercambio. Esto mejoró sustancialmente la capacidad de respuesta de estos sistemas operativos.

En las arquitecturas x86 (desde 80486 ) e Itanium esto se implementa como instrucción de comparación e intercambio ( CMPXCHG ) (en un multiprocesador se debe utilizar el prefijo LOCK ).

A partir de 2013, la mayoría de las arquitecturas multiprocesador admiten CAS en hardware, y la operación de comparación e intercambio es la primitiva de sincronización más popular para implementar estructuras de datos concurrentes basadas en bloqueos y no bloqueantes . [4]

Las operaciones de contador atómico y máscara de bits atómica en el núcleo Linux suelen utilizar una instrucción de comparación e intercambio en su implementación. Las arquitecturas SPARC-V8 y PA-RISC son dos de las pocas arquitecturas recientes que no admiten CAS en hardware; la adaptación de Linux a estas arquitecturas utiliza un spinlock . [7]

Implementación en C

Muchos compiladores de C admiten el uso de comparación e intercambio ya sea con las funciones C11 <stdatomic.h> , [8] o alguna extensión C no estándar de ese compilador C en particular, [9] o llamando a una función escrita directamente en lenguaje ensamblador utilizando la instrucción de comparación e intercambio.

La siguiente función C muestra el comportamiento básico de una variante de comparación e intercambio que devuelve el valor antiguo de la ubicación de memoria especificada; sin embargo, esta versión no proporciona las garantías cruciales de atomicidad que ofrecería una operación de comparación e intercambio real:

int comparar_e_intercambiar ( int * reg , int valor_antiguo , int valor_nuevo ) { ATÓMICO (); int valor_registro_antiguo = * reg ; si ( valor_registro_antiguo == valor_antiguo ) * reg = valor_nuevo ; FIN_ATÓMICO (); devolver valor_registro_antiguo ; }                     

old_reg_valsiempre se devuelve, pero se puede probar después de la compare_and_swapoperación para ver si coincide oldval, ya que puede ser diferente, lo que significa que otro proceso ha logrado tener éxito en una competencia compare_and_swappara cambiar el valor del registro de oldval.

Por ejemplo, se puede implementar un protocolo de elección de modo que cada proceso verifique el resultado de compare_and_swapcontra su propio PID (= newval). El proceso ganador encuentra compare_and_swapel valor inicial que no es PID (por ejemplo, cero). Para los perdedores, devolverá el PID ganador.

Esta es la lógica en el Manual de software Intel Vol 2A:

bool comparar_e_intercambiar ( int * accum , int * dest , int newval ) { if ( * accum == * dest ) { * dest = newval ; devuelve verdadero ; } else { * accum = * dest ; devuelve falso ; } }                         

Extensiones

Dado que CAS opera en una única ubicación de memoria del tamaño de un puntero , mientras que la mayoría de los algoritmos sin bloqueo y sin espera necesitan modificar múltiples ubicaciones, se han implementado varias extensiones.

Doble comparación e intercambio (DCAS)
Compara dos ubicaciones de memoria no relacionadas con dos valores esperados y, si son iguales, establece ambas ubicaciones en nuevos valores. La generalización de DCAS a múltiples palabras (no adyacentes) se denomina MCAS o CASN. DCAS y MCAS son de interés práctico en la implementación conveniente (concurrente) de algunas estructuras de datos como deques o árboles de búsqueda binaria . [10] [11] Sin embargo, DCAS y MCAS pueden implementarse utilizando la memoria transaccional de hardware más expresiva [12] presente en algunos procesadores recientes como IBM POWER8 o en procesadores Intel que admiten Transactional Synchronization Extensions (TSX).
Comparación e intercambio de doble ancho
Opera en dos ubicaciones adyacentes del tamaño de un puntero (o, equivalentemente, una ubicación dos veces más grande que un puntero). En los procesadores x86 posteriores, las instrucciones CMPXCHG8B y CMPXCHG16B [13] cumplen esta función, aunque las primeras CPU AMD de 64 bits no admitían CMPXCHG16B (las CPU AMD modernas sí lo hacen). Algunas placas base Intel de la era Core 2 también dificultan su uso, aunque los procesadores lo admitan. Estos problemas salieron a la luz en el lanzamiento de Windows 8.1 porque requería compatibilidad de hardware con CMPXCHG16B. [14]
Comparación simple, intercambio doble
Compara un puntero pero escribe dos. La instrucción cmp8xchg16 de Itanium implementa esto, [15] donde los dos punteros escritos son adyacentes.
Comparación e intercambio de varias palabras
Es una generalización de la comparación e intercambio normal. Se puede utilizar para intercambiar atómicamente una cantidad arbitraria de ubicaciones de memoria ubicadas arbitrariamente. Por lo general, la comparación e intercambio de múltiples palabras se implementa en software mediante operaciones normales de comparación e intercambio de doble ancho. [16] La desventaja de este enfoque es la falta de escalabilidad.
Comparación e intercambio persistentes
Es una combinación de la operación de persistencia y la operación normal de comparación e intercambio. Se puede utilizar para comparar e intercambiar atómicamente un valor y luego conservarlo, de modo que no haya una brecha entre la visibilidad concurrente y la visibilidad de fallas. La extensión resuelve el problema de lectura de escritura no persistente . [17]

Véase también

Referencias

  1. ^ ab Mullender, Sape; Cox, Russ (2008). Semáforos en Plan 9 (PDF) . Tercer Taller Internacional sobre Plan 9 .
  2. ^ Herlihy, Maurice (enero de 1991). "Sincronización sin espera" (PDF) . ACM Trans. Program. Lang. Syst . 13 (1): 124–149. CiteSeerX 10.1.1.56.5659 . doi :10.1145/114005.102808. S2CID  2181446. Consultado el 20 de mayo de 2007 . 
  3. ^ JH Anderson y M. Moir. "Construcciones universales para operaciones con múltiples objetos". En Proc. 14th Annual ACM Symposium on Principles of Distributed Computing , páginas 184–193, 1995. Véase la Tabla 1, las Figuras 1 y 2 y la Sección 2 en particular.
  4. ^ ab Dice, Dave; Hendler, Danny; Mirsky, Ilya (2013). "Gestión de contención ligera para operaciones de comparación e intercambio eficientes". arXiv : 1305.5800 [cs.DC].
  5. ^ Goetz, Brian (23 de noviembre de 2004). "Teoría y práctica de Java: hacia lo atómico". IBM developerWorks .
  6. ^ Tudor David, Rachid Guerraoui y Vasileios Trigonakis. "Todo lo que siempre quiso saber sobre sincronización pero tenía miedo de preguntar". Actas del vigésimo cuarto simposio de la ACM sobre principios de sistemas operativos . ACM, 2013, págs. 33-48. Detalles en la pág. 34
  7. ^ David S. Miller. "Semántica y comportamiento de operaciones atómicas y de máscara de bits, para mantenedores de puertos de Linux" Archivado el 20 de marzo de 2012 en Wayback Machine .
  8. ^ "intercambio_de_comparación_atómica_débil, intercambio_de_comparación_atómica_fuerte, intercambio_de_comparación_atómica_débil_explícito, intercambio_de_comparación_atómica_fuerte_explícito". es.cppreference.com .
  9. ^ "Extensiones GNU C para la familia de lenguajes C: funciones integradas para acceso a memoria atómica"
  10. ^ Simon Doherty et al., "DCAS no es una bala de plata para el diseño de algoritmos sin bloqueo". 16.º simposio anual de la ACM sobre paralelismo en algoritmos y arquitecturas, 2004, págs. 216-224. doi :10.1145/1007912.1007945
  11. ^ Keir Fraser (2004), "Libertad de encierro en la práctica" UCAM-CL-TR-579.pdf
  12. ^ Dave Dice, Yossi Lev, Mark Moir, Dan Nussbaum y Marek Olszewski. (2009) "Experiencia temprana con una implementación de memoria transaccional de hardware comercial". Informe técnico de Sun Microsystems (60 pp.) SMLI TR-2009-180. Una versión corta apareció en ASPLOS'09 doi :10.1145/1508244.1508263. El informe completo analiza cómo implementar DCAS utilizando HTM en la sección 5.
  13. ^ "Manual del desarrollador de software de arquitecturas Intel 64 e IA-32, volumen 2A: Referencia del conjunto de instrucciones, AM" (PDF) . Consultado el 15 de diciembre de 2007 .
  14. ^ Chacos, Brad (29 de octubre de 2013). "Los nuevos requisitos de Windows 8.1 dejan varados a algunos usuarios en Windows 8". PC World . Archivado desde el original el 16 de enero de 2024.
  15. ^ "Manual del desarrollador de software de la arquitectura Intel Itanium, volumen 3: Referencia del conjunto de instrucciones" (PDF) . Consultado el 15 de diciembre de 2007 .
  16. ^ "Una operación práctica de comparación e intercambio de varias palabras" (PDF) . Consultado el 8 de agosto de 2009 .
  17. ^ Wang, William; Diestelhorst, Stephan (17 de junio de 2019). "Atómica persistente para implementar estructuras de datos duraderas sin bloqueos para memoria no volátil (anuncio breve)". 31.° simposio de la ACM sobre paralelismo en algoritmos y arquitecturas . Association for Computing Machinery. págs. 309–311. doi :10.1145/3323165.3323166. ISBN . 9781450361842. S2CID  195064876 – vía Biblioteca Digital ACM.

Enlaces externos

Algoritmos básicos implementados utilizando CAS

Implementaciones de CAS