2011-01-13 8 views
6

Si tengo un código que se ve algo como:siguientes puntos en un entorno multiproceso

typedef struct { 
    bool some_flag; 

    pthread_cond_t c; 
    pthread_mutex_t m; 
} foo_t; 

// I assume the mutex has already been locked, and will be unlocked 
// some time after this function returns. For clarity. Definitely not 
// out of laziness ;) 
void check_flag(foo_t* f) { 
    while(f->flag) 
     pthread_cond_wait(&f->c, &f->m); 
} 

¿Hay algo en el estándar de C prevenir un optimizador de la reescritura check_flag como:

void check_flag(foo_t* f) { 
    bool cache = f->flag; 
    while(cache) 
     pthread_cond_wait(&f->c, &f->m); 
} 

En otra palabras, ¿el código generado tiene para seguir el puntero f cada vez que pasa el ciclo, o es el compilador libre de sacar la desreferencia?

Si es es libre de sacarlo, ¿hay alguna manera de evitar esto? ¿Necesito rociar una palabra clave volátil en alguna parte? No puede ser el parámetro check_flag porque planeo tener otras variables en esta estructura que no me importe que el compilador optimice de esta manera.

puedo tener que recurrir a:

void check_flag(foo_t* f) { 
    volatile bool* cache = &f->some_flag; 
    while(*cache) 
     pthread_cond_wait(&f->c, &f->m); 
} 
+0

+1 por pensar en este tipo de problema antes de escribir código enhebrado por prueba y error. –

Respuesta

3

Normalmente, usted debe tratar de bloquear el mutex pthread antes de esperar en el objeto de condición como la liberación pthread_cond_wait llamada de la exclusión mutua (y adquirir de nuevo antes de volver). Por lo tanto, su función check_flag se debe reescribir así para cumplir con la semántica en la condición pthread.

void check_flag(foo_t* f) { 
    pthread_mutex_lock(&f->m); 
    while(f->flag) 
     pthread_cond_wait(&f->c, &f->m); 
    pthread_mutex_unlock(&f->m); 
} 

En cuanto a la cuestión de si o no el compilador está permitido optimizar la lectura del campo flag, este answer lo explica con más detalle de lo que pueda.

Básicamente, el compilador conoce la semántica de pthread_cond_wait, pthread_mutex_lock y pthread_mutex_unlock. Él sabe que no puede optimizar la lectura de la memoria en esa situación (la llamada al pthread_cond_wait en este ejemplo). Aquí no hay ninguna noción de barrera de la memoria, solo un conocimiento especial de ciertas funciones y alguna regla a seguir en su presencia.

Hay otra cosa que lo protege de la optimización realizada por el procesador. El procesador promedio es capaz de reordenar el acceso a la memoria (lectura/escritura) siempre que se conserve la semántica, y siempre lo hace (ya que permite aumentar el rendimiento). Sin embargo, este descanso cuando más de un procesador puede acceder a la misma dirección de memoria. Una barrera de memoria es solo una instrucción para el procesador diciéndole que puede mover la lectura/escritura que se emitió antes de la barrera y ejecutarla después de la barrera. Los ha terminado ahora.

+0

¿Eso significa que el compilador no puede almacenar en caché el valor de 'p-> some_flag' en un registro? No estoy seguro de las implicaciones de una barrera de memoria. ¿Te importa explicarlos un poco? –

+0

Editado la respuesta. ¿Está más claro ahora? –

+0

Sí, gracias. –

3

Como está escrito, el compilador puede cachear el resultado como usted describe o incluso de una manera más sutil, al ponerlo en un registro. Puede evitar que esta optimización tenga lugar al hacer la variable volatile. Pero eso no es necesariamente suficiente. ¡No debe codificarlo de esta manera! Debe usar las variables de condición según lo prescrito (bloquear, esperar, desbloquear).

Tratar de trabajar en la biblioteca es malo, pero empeora. Tal vez leyendo el artículo de Hans Boehm sobre el tema general del PLDI 2005 ("Los hilos no se pueden implementar como una biblioteca"), o muchos de sus follow-on articles (que conducen a un modelo de memoria C++ revisado) pondrán el temor de Dios en usted y conducirlo de vuelta a la recta y estrecha :).

+0

No puedo leer ese papel sin pagar dinero.Soy demasiado pobre para aprender =/ –

+1

Enlace alternativo alternativo al informe técnico: http://www.hpl.hp.com/techreports/2004/HPL-2004-209.html – EmeryBerger

+0

Gracias por el enlace. Me encanta leer los papeles de Hans. Aunque son demasiado divertidos. Voy a perder una hora de mi vida. –

7

En el caso general, aunque multi-threading no estuvo involucrado y que el rulo se parecía a:

void check_flag(foo_t* f) { 
    while(f->flag) 
     foo(&f->c, &f->m); 
} 

el compilador sería incapaz de almacenar en caché la prueba f->flag. Esto se debe a que el compilador no puede saber si una función (como foo() anterior) puede cambiar el objeto al que apunta f.

En circunstancias especiales (foo() es visible para el compilador, y se sabe que no tener un alias o de otra manera modificable por foo() todos los punteros pasados ​​a la check_flag()) el compilador podría ser capaz de optimizar el cheque.

Sin embargo, pthread_cond_wait() debe implementarse de forma que impida esa optimización.

Ver Does guarding a variable with a pthread mutex guarantee it's also not cached?:

Usted también puede estar interesado en la respuesta de Steve Jessop a: Can a C/C++ compiler legally cache a variable in a register across a pthread library call?

Pero lo lejos que quiere tomar las cuestiones planteadas por el papel de Boehm en su propio trabajo depende de usted. Por lo que puedo decir, si quiere tomar la posición de que pthreads no/no puede hacer la garantía, entonces, en esencia, está asumiendo la postura de que pthreads es inútil (o al menos no ofrece garantías de seguridad, que Creo que por reducción tiene el mismo resultado). Si bien esto podría ser cierto en el sentido más estricto (como se aborda en el documento), probablemente tampoco sea una respuesta útil. No estoy seguro de qué opción tendrías aparte de pthreads en plataformas basadas en Unix.

+0

Ojalá pudiera aceptar dos respuestas, pero no puedo. Acabo de elegir el que tenga el representante más bajo. ¡Eran igualmente útiles! –

+1

Esta es la mejor respuesta, pero me gusta la lógica de OP para aceptar la otra. :-) Por lo que vale, las funciones de sincronización pthread se especifican como barreras de memoria completa. Cómo se implementa esto no es asunto de la aplicación; simplemente está garantizado que funciona. –

+0

"_pthread_cond_wait() debe implementarse de una manera que evite esa optimización._" Tengo curiosidad por cómo 'pthread_cond_wait' podría implementarse razonablemente de una manera que permita la optimización (incorrecta). – curiousguy

1

Volatile es para este propósito. Confiar en el compilador para conocer las prácticas de codificación pthread me parece un poco loco, aunque; los compiladores son bastante inteligentes en estos días. De hecho, el compilador probablemente vea que está realizando un bucle para probar una variable y no la guardará en un registro por ese motivo, no porque lo vea usando pthreads. Solo usa volátiles si realmente te importa.

Tipo de pequeña nota graciosa. Tenemos un VOLATILE #define que es "volátil" (cuando creemos que el error no puede ser nuestro código ...) o en blanco. Cuando creemos que tenemos un colapso debido a que el optimizador nos está matando, lo definimos como "volátil", lo que coloca a la volatilidad frente a casi todo. Luego probamos para ver si el problema desaparece. Hasta ahora ... ¡los errores han sido el desarrollador y no el compilador! ¿¡Quién hubiera pensado !? Hemos desarrollado una biblioteca de threading de alto rendimiento "sin bloqueo" y "sin bloqueo". Tenemos una plataforma de prueba que la lleva al punto de miles de carreras por segundo. ¡Así que, nunca hemos detectado un problema que requiera volatilidad! Hasta ahora, gcc nunca ha almacenado en caché una variable compartida en un registro. yah ... estamos sorprendidos también. ¡Todavía estamos esperando nuestra oportunidad de usar volatilidad!

+0

Me gusta la historia =) tener un voto favorable. –

+0

"_compilers son bastante inteligentes en estos días._" ¿Cuándo el compilador asumió que una llamada a una función no tenía efecto sobre nada? – curiousguy

Cuestiones relacionadas