threads lock book c# c++ multithreading locking spinlock

c# - book - Spinlocks, ¿Qué tan útiles son?



multithreading c# book (9)

Casi nunca necesitas usar los spinlocks en el código de la aplicación, si es que debes evitarlos.

No puedo pensar en ninguna razón para usar un spinlock en el código c # que se ejecuta en un sistema operativo normal. La mayoría de los bloqueos ocupados son un desperdicio en el nivel de la aplicación: el giro puede hacer que use toda la división de tiempo de la CPU, ya que un bloqueo provocará un cambio de contexto si es necesario.

El código de alto rendimiento en el que tiene nr de hilos = nr de procesadores / núcleos podría beneficiarse en algunos casos, pero si necesita una optimización del rendimiento a ese nivel, es probable que esté haciendo la próxima generación de juegos 3D, trabajando en un sistema operativo integrado con primitivas de sincronización deficientes, creando un Sistema operativo / controlador o en cualquier caso no utilizando c #.

¿Con qué frecuencia te encuentras realmente usando spinlocks en tu código? ¿Qué tan común es encontrarse con una situación en la que el uso de un bucle ocupado realmente supera el uso de los bloqueos?
Personalmente, cuando escribo algún tipo de código que requiere seguridad de subprocesos, tiendo a compararlo con diferentes primitivas de sincronización, y en lo que va, parece que usar bloqueos ofrece un mejor rendimiento que usar cierres giratorios. No importa el poco tiempo que realmente retenga el bloqueo, la cantidad de contención que recibo cuando uso los spinlocks es mucho mayor que la cantidad que obtengo al usar los bloqueos (por supuesto, ejecuto mis pruebas en una máquina multiprocesador).

Me doy cuenta de que es más probable encontrar un spinlock en el código de "bajo nivel", pero me interesa saber si le resulta útil incluso en un tipo de programación de nivel más alto.


Depende de lo que estés haciendo. En el código de aplicación general, querrás evitar los spinlocks.

En cosas de bajo nivel en las que solo mantendrás el bloqueo durante un par de instrucciones, y la latencia es importante, una alfombrilla de cierre giratorio puede ser una mejor solución que un bloqueo. Pero esos casos son raros, especialmente en el tipo de aplicaciones donde normalmente se usa C #.


En C #, los "bloqueos de giro" han sido, en mi experiencia, casi siempre peores que el bloqueo, es raro que los bloqueos de giro superen al bloqueo.

Sin embargo, ese no es siempre el caso. .NET 4 está agregando una estructura System.Threading.SpinLock . Esto proporciona beneficios en situaciones en las que se mantiene un bloqueo por un tiempo muy corto y se agarra repetidamente. De los documentos de MSDN en Estructuras de datos para la programación paralela :

En escenarios en los que se espera que la espera del bloqueo sea breve, SpinLock ofrece un mejor rendimiento que otras formas de bloqueo.

Los bloqueos de giro pueden superar a otros mecanismos de bloqueo en los casos en los que está haciendo algo como bloquear a través de un árbol: si solo tiene bloqueos en cada nodo durante un período de tiempo muy, muy corto, pueden realizar un bloqueo tradicional. Me encontré con esto en un motor de renderizado con una actualización de escena multiproceso, en un punto: giros de bloqueo perfilados para superar el bloqueo con Monitor.Enter.


Mi 2c: Si sus actualizaciones satisfacen algunos criterios de acceso, entonces son buenos candidatos para el bloqueo de giro:

  • rápido , es decir, tendrá tiempo para adquirir el spinlock, realizar las actualizaciones y liberar el spinlock en un solo hilo cuanta para que no se anule mientras mantiene el spinlock
  • todos los datos que actualiza están localizados preferiblemente en una sola página que ya está cargada, no desea que se pierda un TLB mientras mantiene el spinlock, y definitivamente no desea que se lea un intercambio de fallos de página.
  • atómico no necesita ningún otro bloqueo para realizar la operación, es decir. Nunca espere por cerraduras bajo spinlock.

Para cualquier cosa que tenga potencial de rendimiento, debe utilizar una estructura de bloqueo notificada (eventos, exclusión mutua, semáforos, etc.).


Para mi trabajo en tiempo real, particularmente con los controladores de dispositivos, los he usado un poco. Resulta que (la última vez que cronometré esto), esperar un objeto de sincronización como un semáforo vinculado a una interrupción de hardware mastica por lo menos 20 microsegundos, sin importar cuánto demore realmente la interrupción. Una sola comprobación de un registro de hardware asignado en memoria, seguida de una verificación a RDTSC (para permitir un tiempo de espera para que no se bloquee la máquina) se encuentra en el rango alto de nannosegundos (básicamente en el ruido). Para un apretón de manos a nivel de hardware que no debería tomar mucho tiempo, es realmente difícil vencer a un spinlock.


Si tiene un código crítico de rendimiento y ha determinado que necesita ser más rápido de lo que es actualmente y ha determinado que el factor crítico es la velocidad de bloqueo, entonces sería una buena idea probar un spinlock. En otros casos, ¿para qué molestarse? Los bloqueos normales son más fáciles de usar correctamente.


Tenga en cuenta los siguientes puntos:

  1. La mayoría de las implementaciones de mutexe giran por un momento antes de que el hilo no esté programado. Debido a esto, es difícil comparar estas mutexes con los spinlocks puros.

  2. Varios hilos que giran "lo más rápido posible" en el mismo spinlock considerarán todo el ancho de banda y disminuirán drásticamente la eficiencia de su programa. Necesitas agregar un pequeño tiempo de "dormir" agregando noop en tu ciclo de giro.


Un caso de uso para los bloqueos de giro es si espera una contención muy baja pero va a tener muchos de ellos. Si no necesita soporte para el bloqueo recursivo, se puede implementar un spinlock en un solo byte, y si la contención es muy baja, entonces el desperdicio del ciclo de la CPU es insignificante.

Para un caso de uso práctico, a menudo tengo matrices de miles de elementos, donde las actualizaciones a diferentes elementos de la matriz pueden ocurrir de forma segura en paralelo. Las probabilidades de que dos subprocesos intenten actualizar el mismo elemento al mismo tiempo son muy pequeñas (baja disputa), pero necesito un bloqueo para cada elemento (voy a tener muchos). En estos casos, normalmente asigno una matriz de ubytes del mismo tamaño que la matriz que estoy actualizando en paralelo e implemento spinlocks en línea como (en el lenguaje de programación D):

while(!atomicCasUbyte(spinLocks[i], 0, 1)) {} myArray[i] = newVal; atomicSetUbyte(spinLocks[i], 0);

Por otro lado, si tuviera que usar bloqueos regulares, tendría que asignar una matriz de punteros a Objetos, y luego asignar un objeto Mutex para cada elemento de esta matriz. En escenarios como el descrito anteriormente, esto es simplemente un desperdicio.


Utilicé cerraduras de giro para la fase de detención del mundo del recolector de basura en mi proyecto HLVM porque son fáciles y esa es una VM de juguete. Sin embargo, los bloqueos de giro pueden ser contraproducentes en ese contexto:

Uno de los errores de perfección del recolector de basura de Glasgow Haskell Compiler es tan molesto que tiene un nombre, la " última desaceleración del núcleo ". Esto es una consecuencia directa de su uso inadecuado de los cierres giratorios en su GC y está exacerbado en Linux debido a su programador, pero, de hecho, el efecto se puede observar cuando otros programas compiten por el tiempo de CPU.

El efecto es claro en el segundo gráfico here y puede verse que afecta más que solo el último núcleo here , donde el programa Haskell ve una degradación del rendimiento más allá de solo 5 núcleos.