stringtranslate.com

Bloqueo giratorio

En ingeniería de software , un spinlock es un bloqueo que hace que un hilo que intenta adquirirlo simplemente espere en un bucle ("gire") mientras verifica repetidamente si el bloqueo está disponible. Dado que el hilo permanece activo pero no está realizando una tarea útil, el uso de dicho bloqueo es una especie de espera activa . Una vez adquiridos, los spinlocks generalmente se mantendrán hasta que se liberen explícitamente, aunque en algunas implementaciones pueden liberarse automáticamente si el hilo en espera (el que mantiene el bloqueo) se bloquea o "se duerme".

Debido a que evitan la sobrecarga de la reprogramación de procesos del sistema operativo o el cambio de contexto , los spinlocks son eficientes si es probable que los subprocesos se bloqueen solo por períodos cortos. Por esta razón, los núcleos de los sistemas operativos a menudo usan spinlocks. Sin embargo, los spinlocks se vuelven inútiles si se mantienen durante períodos más largos, ya que pueden evitar que otros subprocesos se ejecuten y requieran una reprogramación. Cuanto más tiempo mantenga un subproceso un bloqueo, mayor será el riesgo de que el programador del sistema operativo lo interrumpa mientras mantiene el bloqueo. Si esto sucede, otros subprocesos quedarán "girando" (intentando repetidamente adquirir el bloqueo), mientras que el subproceso que mantiene el bloqueo no avanza hacia su liberación. El resultado es un aplazamiento indefinido hasta que el subproceso que mantiene el bloqueo pueda terminar y liberarlo. Esto es especialmente cierto en un sistema de un solo procesador, donde cada subproceso en espera de la misma prioridad probablemente desperdicie su quantum (tiempo asignado en el que un subproceso puede ejecutarse) girando hasta que el subproceso que mantiene el bloqueo finalmente termine.

Implementar spinlocks correctamente es un desafío porque los programadores deben tener en cuenta la posibilidad de acceso simultáneo al bloqueo, lo que podría causar condiciones de carrera . Generalmente, tal implementación es posible solo con instrucciones especiales en lenguaje ensamblador , como operaciones atómicas (es decir, ininterrumpibles) de prueba y configuración y no se puede implementar fácilmente en lenguajes de programación que no admitan operaciones verdaderamente atómicas. [1] En arquitecturas sin tales operaciones, o si se requiere una implementación de lenguaje de alto nivel, se puede usar un algoritmo de bloqueo no atómico, por ejemplo, el algoritmo de Peterson . Sin embargo, tal implementación puede requerir más memoria que un spinlock, ser más lenta para permitir el progreso después del desbloqueo y puede no ser implementable en un lenguaje de alto nivel si se permite la ejecución fuera de orden .

Ejemplo de implementación

El siguiente ejemplo utiliza lenguaje ensamblador x86 para implementar un spinlock. Funcionará en cualquier procesador compatible con Intel 80386 .

; Sintaxis Intelbloqueado: ; La variable de bloqueo. 1 = bloqueado, 0 = desbloqueado. dd 0   spin_lock: mov eax , 1 ; Establece el registro EAX en 1. xchg eax , [ locked ] ; Intercambia atómicamente el registro EAX con la variable de bloqueo. Esto siempre almacenará 1 en el bloqueo, dejando el valor anterior en el registro EAX. test eax , eax ; Prueba EAX consigo mismo. Entre otras cosas, esto establecerá el indicador cero del procesador si EAX es 0. Si EAX es 0, entonces el bloqueo se desbloqueó y simplemente lo bloqueamos. De lo contrario, EAX es 1 y no adquirimos el bloqueo. jnz spin_lock ; Regresa a la instrucción MOV si el indicador cero no está establecido; el bloqueo se bloqueó anteriormente y, por lo tanto , necesitamos girar hasta que se desbloquee. ret ; Se adquirió el bloqueo, regresa a la función que lo llamó.                           spin_unlock: xor eax , eax ; Establece el registro EAX en 0. xchg eax , [ bloqueado ] ; Intercambia atómicamente el registro EAX con la variable de bloqueo. ret ; El bloqueo ha sido liberado.           

Optimizaciones significativas

La sencilla implementación anterior funciona en todas las CPU que utilizan la arquitectura x86. Sin embargo, es posible realizar varias optimizaciones de rendimiento:

En implementaciones posteriores de la arquitectura x86, spin_unlock puede usar de manera segura un MOV desbloqueado en lugar del XCHG bloqueado más lento. Esto se debe a reglas sutiles de ordenamiento de memoria que lo admiten, aunque MOV no es una barrera de memoria completa . Sin embargo, algunos procesadores (algunos procesadores Cyrix , algunas revisiones del Intel Pentium Pro (debido a errores) y sistemas Pentium e i486 SMP anteriores ) harán lo incorrecto y los datos protegidos por el bloqueo podrían corromperse. En la mayoría de las arquitecturas que no son x86, se deben usar instrucciones explícitas de barrera de memoria o atómicas (como en el ejemplo). En algunos sistemas, como IA-64 , hay instrucciones especiales de "desbloqueo" que proporcionan el ordenamiento de memoria necesario.

Para reducir el tráfico de bus entre CPU , el código que intenta adquirir un bloqueo debe realizar un bucle de lectura sin intentar escribir nada hasta que lea un valor modificado. Debido a los protocolos de almacenamiento en caché MESI , esto hace que la línea de caché para el bloqueo se vuelva "compartida"; luego, notablemente, no hay tráfico de bus mientras una CPU espera el bloqueo. Esta optimización es efectiva en todas las arquitecturas de CPU que tienen una caché por CPU, porque MESI está muy extendido. En las CPU con Hyper-Threading, hacer una pausa con rep nopbrinda un rendimiento adicional al indicarle al núcleo que puede trabajar en el otro hilo mientras el bloqueo gira esperando. [2]

Las extensiones de sincronización transaccional y otros conjuntos de instrucciones de memoria transaccional de hardware sirven para reemplazar los bloqueos en la mayoría de los casos. Aunque los bloqueos aún son necesarios como una alternativa, tienen el potencial de mejorar en gran medida el rendimiento al hacer que el procesador maneje bloques enteros de operaciones atómicas. Esta característica está incorporada en algunas implementaciones de mutex, por ejemplo en glibc . La elisión de bloqueo de hardware (HLE) en x86 es una versión debilitada pero compatible con versiones anteriores de TSE, y podemos usarla aquí para bloquear sin perder ninguna compatibilidad. En este caso particular, el procesador puede elegir no bloquear hasta que dos subprocesos entren en conflicto entre sí. [3]

Una versión más simple de la prueba puede utilizar la cmpxchginstrucción en x86 o la __sync_bool_compare_and_swapincorporada en muchos compiladores de Unix.

Con las optimizaciones aplicadas, un ejemplo se vería así:

; En C: while (!__sync_bool_compare_and_swap(&locked, 0, 1)) while (locked) __builtin_ia32_pause(); spin_lock: mov ecx , 1 ; Establece el registro ECX en 1. retry: xor eax , eax ; Poner a cero EAX, porque cmpxchg compara con EAX. XACQUIRE lock cmpxchg [ locked ], ecx ; decide atómicamente: si bloqueado es cero, escribe ECX en él. ; XACQUIRE le indica al procesador que estamos adquiriendo un bloqueo. je out ; Si lo bloqueamos (valor anterior igual a EAX: 0), regresa. pause: mov eax , [ locked ] ; Lee bloqueado en EAX. test eax , eax ; Realiza la prueba de cero como antes. jz retry ; Si es cero, podemos volver a intentarlo. rep nop ; Indicarle a la CPU que estamos esperando en un bucle de giro, para que pueda trabajar en el otro hilo ahora. También se escribe como "pausa". jmp pause ; Continúa con la pausa de verificación. out: ret ; Listo.                                      spin_unlock: XRELEASE mov [ bloqueado ], 0 ; Suponiendo que se aplican las reglas de ordenamiento de memoria, libere la variable de bloqueo con una sugerencia de "liberación de bloqueo". ret ; El bloqueo ha sido liberado.        

En cualquier sistema multiprocesador que utilice el protocolo de contención MESI , este tipo de bloqueo de prueba y prueba y configuración (TTAS) funciona mucho mejor que el enfoque simple de bloqueo de prueba y configuración (TAS). [4]

Con una gran cantidad de procesadores, agregar un retraso de retroceso exponencial aleatorio antes de volver a verificar el bloqueo funciona incluso mejor que TTAS. [4] [5]

Algunos procesadores multinúcleo tienen una instrucción de "bloqueo de giro consciente del consumo de energía" que pone al procesador en reposo y luego lo reactiva en el siguiente ciclo después de que se libera el bloqueo. Un bloqueo de giro que utiliza tales instrucciones es más eficiente y consume menos energía que los bloqueos de giro con o sin un bucle de retroceso. [6]

Alternativas

La principal desventaja de un spinlock es que, mientras se espera para adquirir un bloqueo, se pierde tiempo que podría emplearse de forma productiva en otras cosas. Hay dos formas de evitarlo:

  1. No adquirir el bloqueo. En muchas situaciones es posible diseñar estructuras de datos que no requieran bloqueo , por ejemplo, utilizando datos por subproceso o por CPU y deshabilitando las interrupciones .
  2. Cambiar a un hilo diferente mientras se espera. Esto generalmente implica adjuntar el hilo actual a una cola de hilos que esperan el bloqueo, seguido de cambiar a otro hilo que esté listo para hacer algún trabajo útil. Este esquema también tiene la ventaja de que garantiza que no se produzca una falta de recursos siempre que todos los hilos finalmente renuncien a los bloqueos que adquieren y se puedan tomar decisiones de programación sobre qué hilo debe avanzar primero. Los spinlocks que nunca implican un cambio, utilizables por sistemas operativos en tiempo real , a veces se denominan spinlocks sin procesar . [7]

La mayoría de los sistemas operativos (incluidos Solaris , Mac OS X y FreeBSD ) utilizan un enfoque híbrido llamado " mutex adaptativo ". La idea es utilizar un bloqueo de giro cuando se intenta acceder a un recurso bloqueado por un hilo que se está ejecutando en ese momento, pero ponerlo en suspensión si el hilo no se está ejecutando en ese momento. (Esto último siempre es el caso en sistemas de un solo procesador). [8]

OpenBSD intentó reemplazar los spinlocks con bloqueos de tickets que reforzaban el comportamiento de primero en entrar, primero en salir , sin embargo esto resultó en un mayor uso de CPU en el núcleo y aplicaciones más grandes, como Firefox , se volvieron mucho más lentas. [9] [10]

Véase también

Referencias

  1. ^ Silberschatz, Abraham; Galvin, Peter B. (1994). Conceptos de sistemas operativos (cuarta edición). Addison-Wesley. págs. 176-179. ISBN 0-201-59292-4.
  2. ^ "gcc - Bloqueo de giro x86 usando cmpxchg". Desbordamiento de pila .
  3. ^ "Nuevas tecnologías en la arquitectura Arm" (PDF) . Archivado (PDF) desde el original el 2019-04-02 . Consultado el 2019-09-26 .
  4. ^ de Maurice Herlihy y Nir Shavit. "El arte de la programación multiprocesador". "Cerraduras de giro y contención".
  5. ^ "Ajuste de Boost.Fiber: retroceso exponencial".
  6. ^ John Goodacre y Andrew N. Sloss. "Paralelismo y la arquitectura del conjunto de instrucciones ARM". pág. 47.
  7. ^ Jonathan Corbet (9 de diciembre de 2009). «Resolución del problema de la denominación de Spinlock». LWN.net . Archivado desde el original el 7 de mayo de 2013. Consultado el 14 de mayo de 2013 .
  8. ^ Silberschatz, Abraham; Galvin, Peter B. (1994). Conceptos de sistemas operativos (cuarta edición). Addison-Wesley. pág. 198. ISBN 0-201-59292-4.
  9. ^ Ted Unangst (1 de junio de 2013). «src/lib/librthread/rthread.c - Revisión 1.71». Archivado desde el original el 27 de febrero de 2021. Consultado el 25 de enero de 2022 .
  10. ^ Ted Unangst (6 de mayo de 2016). "Comentario de tedu sobre el bloqueo en WebKit - Langostas".

Enlaces externos