Bloqueo de giro

La versión actual de la página aún no ha sido revisada por colaboradores experimentados y puede diferir significativamente de la versión revisada el 7 de agosto de 2022; las comprobaciones requieren 3 ediciones .

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] .

Implementación física

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:

  • garantía del orden de provisión en el orden de solicitud, una garantía contra el "hambre"
  • en el bucle de sondeo, cada procesador sondea su variable local
  • exactamente 1 operación atómica en la captura y exactamente 1 en la liberación

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.

Detalles de las configuraciones de multiprocesador y monoprocesador

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).

El uso de spinlocks

Casos en los que el uso de spinlocks en el espacio del usuario tiene un efecto tangible:

  • Dentro de la sección del código protegido hay varias variables asociadas, cuyo tiempo de modificación puede ser cientos e incluso miles de veces menor que un cambio de contexto por parte del procesador, lo que es una operación especialmente costosa, especialmente en los sistemas modernos.
  • Bloqueo no de secciones de código , sino de datos (a cada estructura de datos que se debe cambiar atómicamente como un todo, se le asocia un spinlock que la protege)
  • Optimización de código cuando es necesario reducir la carga que se produce debido a cambios de contexto demasiado frecuentes

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:

  • Operaciones de bloqueo largas dentro de la sección de código protegido (la E/S de disco y red puede llevar mucho tiempo según los estándares del procesador)
  • Configuraciones de un solo procesador: el procesador pasa el resto del tiempo en un ciclo inactivo .

Problemas de Spinlock y métodos para resolverlos

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

Alternativas a spinlocks

  • Los spinlocks se utilizan para garantizar que un subproceso tenga acceso exclusivo a una estructura de datos protegida. No distingue entre los hilos en sí, ni entre las operaciones realizadas. Sin embargo, a menudo en aplicaciones reales, los subprocesos se pueden dividir en "lectores" y "escritores". Para este caso asimétrico, es más adecuado utilizar bloqueos de lectura y escritura . La estructura puede ser utilizada simultáneamente por un número ilimitado de subprocesos en el modo de "solo lectura", al mismo tiempo que brinda protección de integridad de datos cuando llega un subproceso de "escritura".
  • También hay algoritmos libres de bloqueo basados ​​en la detección de colisiones atómicas. Están optimizados para el caso optimista, en el que toda la verificación de colisión se reduce a una operación de ensamblador atómico ( Comparar e intercambiar , en arquitectura x86 : el comando cmpxchg )

Otras modificaciones de spinlocks

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.

Notas

  1. ↑ 1 2 IEEE, El grupo abierto. Justificación de las interfaces del sistema , información general  . The Open Group Base Specifications Número 7, edición de 2018 . El Grupo Abierto (2018). Consultado el 20 de junio de 2020. Archivado desde el original el 18 de junio de 2020.
  2. 1 2 Tanenbaum, 2011 , 2.3.3. Active Wait Mutual, Strict Interleaving, p. 156.
  3. Oleg Tsilyurik. Herramientas de programación del kernel: Parte 73. Paralelismo y sincronización. Cerraduras. parte 1 - www.ibm.com, 2013. - 13 de agosto. — Fecha de acceso: 12/06/2019.

Literatura

  • M. Russinovich , D. Salomón. 1 // Componentes internos de Microsoft Windows. - 6ª ed.- San Petersburgo. : Pedro, 2013. - S. 218-222. — 800 s. - ("Clase maestra"). — ISBN 978-5-459-01730-4 .
  • Walter Ellos. Uso del modelo de controlador de Microsoft Windows . - 2ª ed.- San Petersburgo. : Pedro, 2007. - S.  173 -178. — 764 pág. - ISBN 978-5-91180-057-4 .
  • Andrew S. Tanenbaum. Sistemas operativos  modernos = Sistemas operativos modernos. — 3ra edición. - San Petersburgo: Peter: Editorial "Peter", 2011. - S. 165-170. — 1117 pág. — (Clásicos de la informática). — ISBN 9785459007572 .

Véase también