En ingeniería de software , un spinlock es un bloqueo que hace que un hilo que intenta adquirirlo simplemente espere en un bucle ("girar" en inglés "spin") mientras comprueba repetidamente si el bloqueo(lock) está disponible. Como el hilo permanece activo pero no está realizando una tarea útil, el uso de dicho bloqueo es una especie de espera ocupada . Una vez adquiridos, los spinlocks generalmente se mantendrán hasta que se liberen explícitamente, aunque en algunas implementaciones se pueden liberar automáticamente si el hilo que está esperando en (lo que contiene el bloqueo) bloquea, o "se va a dormir".

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 hilos se bloqueen solo por períodos cortos. Por esta razón, los kernels del sistema operativo a menudo usan spinlocks. Sin embargo, los spinlocks se vuelven un desperdicio si se mantienen durante más tiempo, ya que pueden evitar que otros hilos se ejecuten y requieren reprogramación. Cuanto más tiempo un hilo mantiene un bloqueo, mayor es el riesgo de que el programador(scheduler) del sistema operativo interrumpa el hilo mientras mantiene el bloqueo. Si esto sucede, otros hilos se dejarán "girando" (tratando repetidamente de adquirir el candado), mientras que el hilo que sujeta el candado no está avanzando hacia su liberación. El resultado es un aplazamiento indefinido hasta que el hilo que sujeta la cerradura pueda terminar y soltarlo. Esto es especialmente cierto en un sistema de procesador único, donde cada subproceso en espera de la misma prioridad puede desperdiciar su cantidad (tiempo asignado en que se puede ejecutar un subproceso) girando hasta que el subproceso que contiene el bloqueo finalice finalmente.

Implementar bloqueos de giro correctamente ofrece desafíos porque los programadores deben tener en cuenta la posibilidad de acceso simultáneo a la cerradura, lo que podría causar condiciones de carrera . En general, dicha implementación solo es posible con instrucciones especiales en lenguaje de ensamblaje, como operaciones atómicas 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 la 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 .

Implementación de ejemplo editar

El siguiente ejemplo usa el lenguaje de ensamblaje x86 para implementar un spinlock. Funcionará en cualquier procesador compatible con Intel 80386.

; Sintaxis de Intel

locked:                      ; La variable de bloqueo. 1 = bloqueado, 0 = desbloqueado.
     dd      0

spin_lock:
     mov     eax, 1          ; Establece el registro EAX a 1.
     xchg    eax, [locked]   ; Cambia automáticamente el registro EAX
                             ;  con la variable de bloqueo.
                             ; Esto siempre va a guardar 1 en el bloqueo,
                             ;  dejando el valor previo en el registro EAX.
     test    eax, eax        ; Prueba el EAX consigo mismo. Entre otras cosas, esto va
                             ;  a establecer el Indicador Cero del procesador si EAX es 0.
                             ; Si EAX es 0, entonces el bloqueo estaba desbloqueado y
                             ;  lo acabamos de desbloquear.
                             ; En caso contrario, EAX es 1 y no adquirimos el bloqueo.
     jnz     spin_lock       ; Salta hacia la instrucción MOV si el Indicador Cero no está
                             ;  establecido; el bloqueo estaba bloqueado anteriormente,
                             ; entonces debemos girar en círculos (spin) hasta que se desbloquee.
     ret                     ; El bloqueo fue adquirido, se vuelve a la función que llamó
                             ;  al spinlock.

spin_unlock:
     xor     eax, eax        ; Se establece el registro EAX a 0.
     xchg    eax, [locked]   ; Se cambia automáticamente el registro
                             ;  EAX con la variable de bloqueo.
     ret                     ; el bloqueo fue liberado.

Optimizaciones significativas editar

La implementación simple anterior funciona en todas las CPU que usan la arquitectura x86. Sin embargo, varias optimizaciones de rendimiento son posibles:

En las implementaciones posteriores de la arquitectura x86, spin_unlock puede usar de forma segura un MOV desbloqueado en lugar del XCHG bloqueado más lento. Esto se debe a sutiles reglas de ordenación de memoria que lo respaldan, aunque MOV no es una barrera de memoria completa. Sin embargo, algunos procesadores (algunos procesadores Cyrix , algunas revisiones de 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 dañarse. En la mayoría de las arquitecturas que no son x86, se debe utilizar una barrera de memoria explícita o instrucciones atómicas (como en el ejemplo). En algunos sistemas, como IA-64 , hay instrucciones especiales de "desbloqueo" que proporcionan la ordenación de memoria necesaria.

Para reducir el tráfico de bus entre CPU, el código que intenta adquirir un bloqueo debe leer en bucle sin intentar escribir nada hasta que lea un valor cambiado. Debido a los protocolos de caché MESI , esto hace que la línea de caché para el bloqueo se convierta en "Compartida"; entonces, notablemente, no hay tráfico en el bus mientras una CPU espera el bloqueo. Esta optimización es efectiva en todas las arquitecturas de CPU que tienen un caché por CPU, porque MESI está muy extendida.

Alternativas editar

La implementación simple anterior funciona en todas las CPU que usan la arquitectura x86. Sin embargo, varias optimizaciones de rendimiento son posibles:


  1. No adquieras el candado En muchas situaciones, es posible diseñar estructuras de datos que no requieren bloqueo , por ejemplo, mediante el uso de datos por subproceso o por CPU y la desactivación de interrupciones. 
  2. Cambie a un hilo diferente mientras espera. Esto normalmente implica unir el hilo actual a una cola de hilos esperando el bloqueo, seguido de cambiar a otro hilo que está listo para hacer un trabajo útil. Este esquema también tiene la ventaja de que garantiza que la falta de recursos no ocurra siempre que todos los hilos abandonen los bloqueos que adquieren y se pueden tomar decisiones sobre qué hilo debe avanzar primero. Los spinlocks que nunca implican conmutación, utilizables por sistemas operativos en tiempo real , a veces se llaman spinlocks sin formato . [2]

La mayoría de sistemas operativos (incluyendo Solaris, Mac OS X y FreeBSD) utiliza una aproximación híbrida llamó "adaptive mutex". La idea es para utilizar un spinlock cuándo este intentando acceder un recurso cerrado por un hilo que corra actualmente, pero para dormir si el hilo no esta actualmente corriendo. (El último es siempre el caso en sistemas de procesador único.)[1]

OpenBSD intentó reemplazar los "spinlocks" con bloqueos de tickets que aplicaban el comportamiento " primero en entrar, primero en salir" , pero esto dio como resultado un mayor uso de la CPU en el kernel y aplicaciones más grandes, como Firefox , cada vez más lentas. [4] [5]

Ve también editar

Referencias editar

  1. Operating System Concepts (Fourth edición). Addison-Wesley. 1994. p. 198. ISBN 0-201-59292-4. 

Enlaces externos editar