2010-04-27 6 views
11

Me gustaría minimizar la sincronización y escribir código bloqueable cuando sea posible en un proyecto mío. Cuando sea absolutamente necesario, me encantaría sustituir las suspensiones de spinlocks livianas construidas a partir de operaciones atómicas por las cerraduras mutex pthread y win32. Según entiendo, se trata de llamadas al sistema que se encuentran debajo y que podrían provocar un cambio de contexto (que puede ser innecesario para secciones críticas muy rápidas en las que sería preferible girar varias veces).¿Spinlock ligeros construidos a partir de las operaciones atómicas de GCC?

Las operaciones atómicas que me refiero están bien documentados aquí: http://gcc.gnu.org/onlinedocs/gcc-4.4.1/gcc/Atomic-Builtins.html

Aquí se muestra un ejemplo para ilustrar lo que estoy hablando. Imagine un árbol RB con múltiples lectores y escritores posible. RBTree :: exists() es de solo lectura y seguro para subprocesos, RBTree :: insert() requerirá acceso exclusivo de un solo escritor (y ningún lector) para estar seguro. Algunos código:

class IntSetTest 
{ 
private: 
    unsigned short lock; 
    RBTree<int>* myset; 

public: 
    // ... 

    void add_number(int n) 
    { 
     // Aquire once locked==false (atomic) 
     while (__sync_bool_compare_and_swap(&lock, 0, 0xffff) == false); 

     // Perform a thread-unsafe operation on the set 
     myset->insert(n); 

     // Unlock (atomic) 
     __sync_bool_compare_and_swap(&lock, 0xffff, 0); 
    } 

    bool check_number(int n) 
    { 
     // Increment once the lock is below 0xffff 
     u16 savedlock = lock; 
     while (savedlock == 0xffff || __sync_bool_compare_and_swap(&lock, savedlock, savedlock+1) == false) 
      savedlock = lock; 

     // Perform read-only operation  
     bool exists = tree->exists(n); 

     // Decrement 
     savedlock = lock; 
     while (__sync_bool_compare_and_swap(&lock, savedlock, savedlock-1) == false) 
      savedlock = lock; 

     return exists; 
    } 
}; 

(supongamos que no tiene por qué ser una excepción de fallos)

¿Es este código de hecho flujos seguros? ¿Hay algún pros/contras para esta idea? ¿Algún consejo? ¿El uso de spinlocks como este es una mala idea si los hilos no son verdaderamente concurrentes?

Gracias de antemano. ;)

+3

La respuesta que di en una pregunta similar, http://stackoverflow.com/questions/1919135/critical-sections-that-spin-on-posix/1923218#1923218, probablemente será relevante aquí. –

+0

Su respuesta fue definitivamente relevante para la cuestión del uso de spinlocks en general. Parecen una buena idea para las máquinas smp en su caso típico. ¿La situación del peor de los casos (un escritor que deja de ejecutarse durante la sección crítica) se iguala con el caso más probable de que dos hilos simultáneos intenten insertarse al mismo tiempo? ¿Qué ocurre en un entorno de subprocesamiento híbrido en el que los subprocesos del usuario se asignan a varios subprocesos del kernel equivalentes a la cantidad de procesadores lógicos en la máquina? La peor situación posible sería incluso menos probable entonces; ¿no? – Thomas

+0

No estoy seguro de hasta qué punto la cantidad de subprocesos del kernel afecta la probabilidad de encontrarse con problemas de rendimiento. Es posible que el hilo del escritor haya agotado su intervalo de tiempo entre la entrada y la salida del bloqueo, lo que daría lugar al caso del problema sin importar cuántos hilos del kernel haya. En este punto, señalaré que la operación de inserción de árbol RB es O (log (n)), por lo que cuanto mayor sea el árbol, más probabilidades habrá de que ocurra este problema. Además, es más probable que un árbol más grande cause fallas en la página durante la actualización, lo que también haría que el problema fuera más probable. Evitaría los espirales aquí. –

Respuesta

4

Necesita un calificador volatile en lock, y yo también lo convertiría en sig_atomic_t. Sin la volatile calificador, este código:

u16 savedlock = lock; 
    while (savedlock == 0xffff || __sync_bool_compare_and_swap(&lock, savedlock, savedlock+1) == false) 
     savedlock = lock; 

no puede releer lock al actualizar savedlock en el cuerpo del bucle while. Considere el caso de que lock es 0xffff. Luego, savedlock será 0xffff antes de verificar la condición de bucle, por lo que la condición while se cortocircuitará antes de llamar al __sync_bool_compare_and_swap. Como no se invocó __sync_bool_compare_and_swap, el compilador no encuentra una barrera de memoria, por lo que podría suponer razonablemente que el valor de lock no ha cambiado debajo de usted y evitar volver a cargarlo en savedlock.

Re: sig_atomic_t, hay una discusión decente here. Las mismas consideraciones que se aplican a los manejadores de señal también se aplicarían a los hilos.

Con estos cambios, supongo que su código sería seguro para subprocesos. Aun así, recomendaría usar mutexes, ya que realmente no sabe cuánto tiempo tomará su inserción de RB-tree en el caso general (según mis comentarios anteriores en la pregunta).

+0

Esto es interesante. He leído muchos artículos que explican por qué volátil es el mejor amigo de un programa de subprocesos múltiples, y muchos explican por qué volátil no tiene nada que ver con esto y hacer que todo sea volátil simplemente ralentizará el programa. En mi aplicación, se podía acceder a más de la mitad de los datos por cualquier hilo y en cualquier momento. ¿Deberían todos ser realmente volátiles? ¿O es esta la excepción porque está en un círculo cerrado que el compilador podría optimizar para verificar solo el bloqueo una vez? – Thomas

+0

es decir, se llama a una función de imagen (que no está en línea), verifica una variable, luego regresa y se vuelve a llamar rápidamente. En este caso, ¿no sería necesario volátil porque el compilador no podría optimizar el código en varias llamadas? Pero en el ciclo anterior, ¿podría darse cuenta de que el bloqueo nunca podría cambiar y optimizarlo? ¿Tan volátil no tiene nada que ver con el almacenamiento en caché, simplemente le dice al compilador que no optimice el acceso a la memoria? Creo que esto tiene sentido para mí. Por favor confirme o aclare! :) – Thomas

+1

Pasé algún tiempo buscando qué tan volátil funciona ...En pocas palabras, lo que hace es evitar la optimización de los accesos a la memoria, y también evitar el reordenamiento de las operaciones de memoria que involucran variables volátiles. (Las operaciones de memoria que involucran variables no volátiles pueden reordenarse alrededor de aquellas que involucran volátiles. Además, incluso si las escrituras ocurren en orden, una CPU diferente puede notar los nuevos valores en un orden diferente). Esto debería ser suficiente para múltiples -la sincronización con hilos _en este caso_, porque también tiene las rutinas '__sync' que proporcionan una barrera de memoria. –

1

Vale la pena señalar que si está utilizando los mutexes de Win32, desde Vista en adelante se le proporciona un grupo de subprocesos. Dependiendo de para qué use el árbol RB, podría reemplazarlo con eso.

Además, lo que debe recordar es que las operaciones atómicas no son particularmente rápidas. Microsoft dijo que eran un par de cientos de ciclos, cada uno.

En lugar de tratar de "proteger" la función de esta manera, probablemente sería mucho más eficiente simplemente sincronizar los hilos, ya sea cambiando a un enfoque SIMD/grupo de hilos, o simplemente usar un mutex.

Pero, por supuesto, sin ver su código, realmente no puedo hacer más comentarios. El problema con el multihilo es que tienes que ver el modelo completo de alguien para entenderlo.

+0

Bueno, otro punto importante es todo el aspecto "ligero" de esto. Esto es solo un ejemplo, pero en mi código real podría haber millones de estos objetos en algunos casos y no creo que sea práctico crear millones de mutexes pthread o win32. Un int sin escritura de 16 bits no causaría ninguna sobrecarga adicional (debido a la alineación). – Thomas

+0

En realidad, el grupo de subprocesos (http://msdn.microsoft.com/en-us/library/ms684957(VS.85).aspx) ha estado disponible desde Windows 2000. –

+0

Tampoco es práctico utilizar millones de operaciones interconectadas. Todavía estoy pensando que debe rediseñar su modelo de subprocesamiento. Parece que quieres diseñar una clase que sea de alto rendimiento e ignorante. @Billy ONeal - tienes razón. No había notado esa función antes. – Puppy

Cuestiones relacionadas