2012-05-26 10 views
7

Esta es una pregunta de la entrevista, la entrevista se ha realizado.¿Cómo hacer la sincronización del hilo sin usar mutex, semorphore, spinLock y futex?

Cómo hacer la sincronización del hilo sin usar mutex, semorphore, spinLock y futex?

Dados 5 hilos, ¿cómo hacer que 4 de ellos esperen una señal del hilo izquierdo en el mismo punto? significa que cuando todos los hilos (1,2,3,4) se ejecutan en un punto de su función de hilo, se detienen y esperan a que la señal del hilo 5 envíe una señal; de lo contrario, no continuarán.

Mi idea:

Uso variable global bool como una bandera, si el hilo 5 no establece cierto, todos los otros subprocesos esperan en un punto y también establecer su variable de indicador cierto. Después de que el hilo 5 encuentre que todas las variables de indicador de los hilos son verdaderas, lo configurará como indicador var verdadero.

Es una espera ocupada.

¿Alguna idea mejor?

Gracias

the pseudo code: 
bool globalflag = false; 
bool a[10] = {false} ; 
int main() 
{ 
    for (int i = 0 ; i < 10; i++) 
    pthread_create(threadfunc, i) ; 

     while(1) 
     { 
     bool b = true; 
     for (int i = 0 ; i < 10 ; i++) 
     { 
       b = a[i] & b ; 
     } 
     if (b) break; 
    } 
    } 
    void threadfunc(i) 
    { 
    a[i] = true; 
    while(!globalflag); 
    } 
+2

Su tiempo (! GlobalFlag) es el equivalente de un bloqueo de bucle. Pero me temo que no entiendo la pregunta. Si usa una señal, lo que haría, entonces todavía usa una forma de semáforo o mutex (aunque esté oculto por el sistema). Sin esperar una señal, tendría un spinlock. Usar globalflag es bueno si puede trabajar mientras espera y cambiar a una tarea diferente (o tarea adicional) una vez que la bandera es verdadera. –

+0

Además de lo que tiene aquí, probablemente tenga que declarar globalflag como volátil para asegurarse de que el compilador realmente lea la memoria en lugar de simplemente mirar el código y decidir que no puede cambiar el valor. Y use algún tipo de barrera de memoria para garantizar que los valores escritos por un hilo sean visibles en otro, – jcoder

Respuesta

5

empezar con una lista enlazada vacío de subprocesos en espera. El cabezal debe establecerse en 0.

Use CAS, compare y cambie, para insertar un hilo al principio de la lista de camareros. Si el encabezado = -1, no inserte ni espere. Puede usar CAS de manera segura para insertar elementos al principio de una lista vinculada si lo hace bien.

Después de insertarse, el hilo de espera debe esperar en SIGUSR1. Use sigwait() para hacer esto.

Cuando esté listo, el hilo de señalización usa CAS para establecer el jefe de la lista de espera en -1. Esto evita que se agreguen más subprocesos a la lista de espera. Luego, el hilo de señalización itera los hilos en la lista de espera y llama a pthread_kill (& hilo, SIGUSR1) para activar cada hilo en espera.

Si se envía SIGUSR1 antes de una llamada a sigwait, sigwait regresará inmediatamente. Por lo tanto, no habrá una carrera entre agregar un hilo a la lista de espera y llamar a sigwait.

EDIT:

CAS ¿Por qué es más rápido que un mutex? La respuesta de Laymen (soy un hombre común). Es más rápido para algunas cosas en algunas situaciones, ya que tiene una sobrecarga menor cuando NO hay raza. Entonces, si puede reducir su problema concurrente a la necesidad de cambiar 8-16-32-64-128 bits de memoria contigua, y una carrera no va a suceder muy a menudo, CAS gana. CAS es básicamente una instrucción mov un poco más sofisticada/costosa, donde ibas a hacer un "mov" regular de todos modos. Es una "exclusión de bloqueo" o algo así.

Un mutex por otro lado es un montón de cosas extra, que ensucia otras líneas de caché y usa más barreras de memoria, etc. Aunque CAS actúa como una barrera de memoria en el x86, x64, etc. Entonces, por supuesto tienes que desbloquear el mutex, que es probablemente la misma cantidad de material extra.

Aquí es cómo agregar un elemento a una lista enlazada usando CAS:

while (1) 
{ 
    pOldHead = pHead; <-- snapshot of the world. Start of the race. 
    pItem->pNext = pHead; 
    if (CAS(&pHead, pOldHead, pItem)) <-- end of the race if phead still is pOldHead 
    break; // success 
} 

Entonces, ¿Con qué frecuencia piensa su código va a tener múltiples hilos en esa línea CAS exactamente al mismo tiempo? En realidad ... no muy a menudo. Hicimos pruebas que acababan de realizar bucles para agregar millones de elementos con múltiples hilos al mismo tiempo, y eso ocurre mucho menos del 1% del tiempo. En un programa real, puede que nunca suceda.

Obviamente si hay una carrera, tienes que volver atrás y hacer ese ciclo otra vez, pero en el caso de una lista vinculada, ¿qué te cuesta eso?

La desventaja es que no puede hacer cosas muy complejas en esa lista vinculada si va a utilizar ese método para agregar elementos al encabezado. Intente implementar una lista de doble enlace. Que dolor.

EDIT:

En el código anterior utilizo una macro CAS. Si está usando linux, CAS = macro usando __sync_bool_compare_and_swap. Ver gcc atomic builtins. Si está usando Windows, CAS = macro usando algo como InterlockedCompareExchange. Esto es lo que una función en línea en las ventanas podría ser:

inline bool CAS(volatile WORD* p, const WORD nOld, const WORD nNew) { 
    return InterlockedCompareExchange16((short*)p, nNew, nOld) == nOld; 
} 
inline bool CAS(volatile DWORD* p, const DWORD nOld, const DWORD nNew) { 
    return InterlockedCompareExchange((long*)p, nNew, nOld) == nOld; 
} 
inline bool CAS(volatile QWORD* p, const QWORD nOld, const QWORD nNew) { 
    return InterlockedCompareExchange64((LONGLONG*)p, nNew, nOld) == nOld; 
} 
inline bool CAS(void*volatile* p, const void* pOld, const void* pNew) { 
    return InterlockedCompareExchangePointer(p, (PVOID)pNew, (PVOID)pOld) == pOld; 
} 
+0

gracias, ¿esta sincronización tiene una sobrecarga menor que mutex/fuex/semáforo? – user1000107

+0

Lo evalué hace aproximadamente 1-2 años, así que mi memoria se está desvaneciendo. Específicamente, intentamos ver qué tan rápido podíamos dormir y despertar hilos individuales. Este método parecía ser> 10x (¿o era 40x?) Más rápido que mutexes y variables de condición. Comparamos este método con uno que usa una variable mutex/condición individual para cada hilo. Nunca publiqué mi prueba aquí para ver si estaba defectuosa o no ... No probamos futexes ni semáforos. – johnnycrash

+0

¿Podría explicar muy brevemente por qué CAS es más rápido que mutex? Funciona como futex (sin llamada sys si no hay contención de bloqueo?)? ¡Gracias! – user1000107

1
  1. Elija una señal de usar, dicen SIGUSR1.
  2. Utilice pthread_sigmask para bloquear SIGUSR1.
  3. Crear los hilos (heredan la máscara de señal, por lo tanto, 1 debe hacerse primero!)
  4. Los hilos 1-4 llaman a sigwait, bloqueando hasta que se recibe SIGUSR1.
  5. El hilo 5 llama a kill() o pthread_kill 4 veces con SIGUSR1. Como POSIX especifica que las señales se entregarán a un hilo que no está bloqueando la señal, se enviará a uno de los hilos esperando en sigwait(). Por lo tanto, no es necesario realizar un seguimiento de qué subprocesos ya han recibido la señal y cuáles no, con la sincronización asociada.
+0

gracias, parece que pthread_sigmask es una llamada al sistema y esta sincronización tiene una sobrecarga menor que mutex/fuex/semáforo? – user1000107

+0

@ user1000107: No, no lo creo. Los futexes no confinados no requieren una llamada del sistema y deben ser tan rápidos como unas pocas instrucciones de sincronización de la CPU (cas, barreras, etc.), por lo que es difícil superar eso para un mecanismo de sincronización de propósito general. – janneb

+0

pthread_sigmask tiene algunas ventajas sobre mutex, semáforo? Gracias ! – user1000107

1

Usted puede hacer esto utilizando MONITOR y MWAIT instrucciones SSE3, disponibles a través de los _mm_mwait_mm_monitor y los intrínsecos, Intel tiene un artículo sobre ella here. (también hay una patente para usar memory-monitor-wait para contención de bloqueo here que puede ser de interés).

+0

Esta es una característica ingeniosa. Algún día lo sé o algo similar será útil. ¡Gracias! – johnnycrash

+0

@ Necrolis, estas instrucciones son para Windows, ¿y si linux/unix? – user1000107

+0

@ user1000107: estas instrucciones son para ** x86 y SSE3 **, no tienen nada que ver con Windows, simplemente elegí usar los documentos de MSDN para los intrínsecos disponibles en MSVC, deberían ser los mismos en ICC (que funciona bajo Linux y OSX) – Necrolis

Cuestiones relacionadas