Un bloqueo de giro o spinlock ( inglés spinlock - bloqueo cíclico) es una primitiva de sincronización de bajo nivel [1] utilizada en sistemas multiprocesador para implementar la exclusión mutua de la ejecución de secciones críticas de código utilizando un bucle de espera activo [2] . Se utiliza en los casos en que se espera que la espera de un bloqueo sea corta [2] o si el contexto de ejecución no permite la transición a un estado bloqueado [3] .
Los spinlocks son similares a los mutexes , lo que le permite pasar menos tiempo bloqueando un hilo, ya que no necesita transferir el hilo al estado bloqueado. En el caso de mutexes, puede ser necesario invocar el programador para cambiar el estado del subproceso y agregarlo a la lista de subprocesos que esperan ser desbloqueados. Los spinlocks no usan el programador y usan un ciclo de espera sin cambiar el estado del subproceso, lo que desperdicia tiempo de CPU esperando que otro subproceso libere el bloqueo. Una implementación típica de un spinlock es una verificación cíclica simple de la disponibilidad de la variable spinlock [1] .
Físicamente, un spinlock es una variable en la memoria y se implementa en operaciones atómicas que deben estar presentes en el conjunto de instrucciones del procesador . Cada procesador que quiere acceder al recurso compartido escribe atómicamente el valor condicional " ocupado " en esta variable, utilizando un análogo de la operación de intercambio (en la arquitectura x86 - xchg). Si el valor anterior de la variable (devuelto por el comando) era " libre ", se considera que el procesador dado ha accedido al recurso; de lo contrario, el procesador vuelve a la operación de intercambio y recorre el spinlock hasta que se libera. Después de trabajar con un recurso compartido, el procesador, el propietario del spinlock, debe escribir el valor condicional " free " en él.
Un ejemplo de implementación de un spinlock en ensamblador x86:
mov eax , spinlock_address mov ebx , SPINLOCK_BUSY ciclo_espera: xchg [ eax ], ebx ; xchg es la única instrucción que es atómica sin el prefijo lock cmp ebx , SPINLOCK_FREE jnz wait_cycle ; <la sección crítica está capturada por este hilo, el trabajo con el recurso compartido está en progreso aquí> mov eax , spinlock_address mov ebx , SPINLOCK_FREE xchg [ eax ], ebx ; use xchg para el cambio atómico ; Las últimas 3 instrucciones deben reemplazarse con mov [spinlock_address], SPINLOCK_FREE - ; esto aumentará la velocidad debido a la ausencia de bloqueos de bus innecesarios, y mov se ejecutará atómicamente de todos modos ; (pero solo si spinlock_address está alineado en un límite de dword)Una implementación más inteligente usaría una operación regular en lugar de una operación atómica para sondear en un bucle y una operación atómica solo para intentos de captura. El hecho es que la implementación de las operaciones de memoria atómica ocurre cuando el hardware bloquea el bus del sistema por parte del procesador durante la duración de la operación atómica (que incluye lectura, modificación y escritura). Durante estas tres operaciones, no se pueden realizar otras operaciones en el bus, lo que reduce el rendimiento de otros procesadores en el sistema (si comparten un bus común ), incluso si no tienen nada que ver con este spinlock.
También se utilizan los llamados. spinlocks en cola - "spinlocks en cola". En lugar de asignar 0 o 1 a una variable atómica, utilizan una adición atómica de una estructura al encabezado de la lista, mientras que el encabezado de la lista es una variable atómica del tipo "puntero".
Propiedades útiles de spinlocks en cola:
Los spinlocks se utilizan para sincronizar pequeñas secciones de código cuando el uso de mecanismos más complejos no es razonable o es imposible. La implementación de las primitivas de sincronización y el administrador de subprocesos requiere necesariamente bloqueos para proteger las listas de subprocesos que están listos para ejecutarse y las listas de subprocesos que están esperando objetos. Tal bloqueo solo puede ser un bloqueo giratorio debido a su nivel muy bajo. Por tanto, el spinlock es la primitiva de sincronización más baja en la que se basa la implementación de todas las demás.
Las versiones de Windows desde Windows 7 inclusive utilizan el paradigma de estructuras de datos sin bloqueo para implementar el despachador/programador. Por lo tanto, se salvan del único spinlock global KiDispatcherLock, uno de los más cargados en el kernel del sistema operativo.
Existe una opinión generalizada de que en las aplicaciones de usuario que se ejecutan en un sistema operativo multitarea, el uso de spinlocks es inaceptable, ya que esperar a que se libere un spinlock conduce a una espera activa en un bucle que desperdicia recursos informáticos de la CPU, y las primitivas de alto nivel deben ser se utiliza para sincronizar programas de usuario, lo que implica una espera pasiva: si un subproceso determinado no puede continuar con la ejecución, entonces le da control al sistema operativo y no gira en un ciclo de espera de bloqueo de giro (que puede ser potencialmente infinito). De hecho, esta afirmación es 100% cierta solo para sistemas monoprocesador. En muchos casos, el uso de spinlocks en configuraciones SMP conduce a ganancias de eficiencia si sondear y adquirir un spinlock es más rápido que llamar a una adquisición mutex en el kernel.
El criterio principal aquí es la contención: la "rigidez" de la competencia por el recurso. Un recurso ligeramente cargado que no es un sitio de ejecución popular se comporta de manera diferente a un recurso muy cargado que se captura y desasigna con mucha frecuencia.
Además, en el mismo Windows, hay variedades de mutexes (por ejemplo, el conocido CRITICAL_SECTION/EnterCriticalSection/LeaveCriticalSection, o su sinónimo en el kernel del sistema operativo - FAST_MUTEX/ExAcquireFastMutex/ExReleaseFastMutex), que primero funcionan como spinlock, usando una encuesta de valor en la memoria, y solo entonces, después de una gran cantidad de encuestas, vaya al kernel para bloquear la espera. Dichos objetos combinan las mejores cualidades de spinlocks (costo mínimo de captura) y mutexes (sin desperdicio de recursos de CPU para sondeos).
Casos en los que el uso de spinlocks en el espacio del usuario tiene un efecto tangible:
Sin embargo, el uso de "mutexes rápidos" como CRITICAL_SECTION de Win32 hace que todo lo anterior sea innecesario en el espacio del usuario.
Casos en los que el uso de spinlocks no está justificado y es una pérdida de recursos del procesador:
En los procesadores modernos, el ciclo de suspensión puede ser muy rápido debido a las peculiaridades de la arquitectura canalizada que, además de los ciclos inactivos, puede provocar un calentamiento más intenso que durante el funcionamiento normal.
Pentium 4 y modelos posteriores de procesadores Intel introdujeron una instrucción de ensamblador especial para insertar dentro de un bucle de pausa ( opcode 0xf3 0x90, similar a rep nop para compatibilidad con procesadores más antiguos) que tiene como objetivo indicar al procesador que este ciclo es un bucle de espera y permite que el procesador admita varios subprocesos en el mismo núcleo para pasar al siguiente subproceso.
Las versiones de Windows desde Windows 7 están optimizadas para ejecutarse como "invitado" en una máquina virtual, y en lugar de hacer una pausa en los casos en que el sistema operativo se ejecuta como invitado, una llamada especial "notifica al hipervisor que estamos en un ciclo de espera". se usa
Spinlock con crecimiento automático hasta que se captura un mutex completo después de que haya expirado un cierto número de revoluciones del ciclo se usa, por ejemplo, en secciones críticas de Windows para la optimización, que consiste en la ausencia de llamadas al mutex en ausencia de competencia para un recurso.