2010-12-18 5 views
9

Supongamos que tenemos el siguiente fragmento de código:¿Puede un compilador C/C++ almacenar en caché legalmente una variable en un registro a través de una llamada de biblioteca pthread?

#include <pthread.h> 
#include <stdio.h> 
#include <stdlib.h> 

void guarantee(bool cond, const char *msg) { 
    if (!cond) { 
     fprintf(stderr, "%s", msg); 
     exit(1); 
    } 
} 

bool do_shutdown = false; // Not volatile! 
pthread_cond_t shutdown_cond = PTHREAD_COND_INITIALIZER; 
pthread_mutex_t shutdown_cond_mutex = PTHREAD_MUTEX_INITIALIZER; 

/* Called in Thread 1. Intended behavior is to block until 
trigger_shutdown() is called. */ 
void wait_for_shutdown_signal() { 

    int res; 

    res = pthread_mutex_lock(&shutdown_cond_mutex); 
    guarantee(res == 0, "Could not lock shutdown cond mutex"); 

    while (!do_shutdown) { // while loop guards against spurious wakeups 
     res = pthread_cond_wait(&shutdown_cond, &shutdown_cond_mutex); 
     guarantee(res == 0, "Could not wait for shutdown cond"); 
    } 

    res = pthread_mutex_unlock(&shutdown_cond_mutex); 
    guarantee(res == 0, "Could not unlock shutdown cond mutex"); 
} 

/* Called in Thread 2. */ 
void trigger_shutdown() { 

    int res; 

    res = pthread_mutex_lock(&shutdown_cond_mutex); 
    guarantee(res == 0, "Could not lock shutdown cond mutex"); 

    do_shutdown = true; 

    res = pthread_cond_signal(&shutdown_cond); 
    guarantee(res == 0, "Could not signal shutdown cond"); 

    res = pthread_mutex_unlock(&shutdown_cond_mutex); 
    guarantee(res == 0, "Could not unlock shutdown cond mutex"); 
} 

Puede un C compatible con los estándares/C++ siempre almacenar en caché el valor de do_shutdown en un registro a través de la llamada a pthread_cond_wait()? Si no, ¿qué normas/cláusulas garantizan esto?

El compilador podría hipotéticamente saber que pthread_cond_wait() no modifica do_shutdown. Esto parece bastante improbable, pero no conozco ningún estándar que lo impida.

En la práctica, ¿los compiladores C/C++ guardan en caché el valor de do_shutdown en un registro a través de la llamada al pthread_cond_wait()?

¿Qué función llama es el compilador garantizado para no almacenar en caché el valor de do_shutdown a través? Está claro que si la función se declara externamente y el compilador no puede acceder a su definición, no debe hacer suposiciones sobre su comportamiento, por lo que no puede probar que no tiene acceso al do_shutdown. Si el compilador puede alinear la función y demostrar que no tiene acceso al do_shutdown, ¿puede caché do_shutdown incluso en una configuración multiproceso? ¿Qué pasa con una función no en línea en la misma unidad de compilación?

+1

Sí, pero puede hacerlo si, y solo si no hay manera legítima de que la función de la biblioteca pueda cambiar el valor de la variable (por ejemplo, si es una variable automática y su dirección nunca se toma). –

+1

@R: corregir ... y en ese caso, es seguro hacerlo, ya que ningún otro hilo podría estar usando esa variable tampoco. –

Respuesta

6

Por supuesto, los estándares actuales de C y C++ no dicen nada sobre el tema.

Por lo que sé, Posix aún evita formalmente la definición de un modelo de simultaneidad (sin embargo, puedo estar desactualizado, en cuyo caso aplicar mi respuesta solo a versiones anteriores de Posix). Por lo tanto, lo que dice debe leerse con un poco de simpatía; no establece con precisión los requisitos en esta área, pero se espera que los implementadores "sepan lo que significan" y hagan algo que haga que los hilos se puedan usar.

Cuando el estándar dice que los mutex "sincronizan el acceso a la memoria", las implementaciones deben suponer que esto significa que los cambios realizados bajo el bloqueo en un hilo serán visibles bajo el bloqueo en otros hilos. En otras palabras, es necesario (aunque no suficiente) que las operaciones de sincronización incluyan barreras de memoria de un tipo u otro, y el comportamiento necesario de una barrera de memoria es que debe asumir que las variables globales pueden cambiar.

Threads Cannot be Implemented as a Library cubre algunos problemas específicos que se requieren para que un pthreads sea realmente utilizable, pero no se mencionan explícitamente en el estándar de Posix en el momento de la redacción (2004). Se vuelve muy importante si su compilador-escritor, o quien definió el modelo de memoria para su implementación, está de acuerdo con Boehm en lo que significa "utilizable", en términos de permitirle al programador "razonar convincentemente sobre la corrección del programa".

Tenga en cuenta que Posix no garantiza una memoria caché coherente, así que si su aplicación perversamente quiere almacenar en caché do_something en un registro en el código, a continuación, incluso si el que marcó volátil, podría elegir perversamente no ensuciar su La memoria caché local de la CPU entre la operación de sincronización y la lectura do_something. Entonces, si el hilo del escritor se ejecuta en una CPU diferente con su propio caché, es posible que no vea el cambio incluso entonces.

Esa es (una razón) por la cual los hilos no se pueden implementar simplemente como una biblioteca. Esta optimización de obtención de un volátil global solo desde la memoria caché de la CPU local sería válida en una implementación de C de subproceso único [*], pero rompe el código de subprocesos múltiples.Por lo tanto, el compilador necesita "saber sobre" los hilos, y cómo afectan otras características del lenguaje (por ejemplo fuera de pthreads: en Windows, donde el caché siempre es coherente, Microsoft explica la semántica adicional que otorga volatile en código multiproceso) Básicamente, debe suponer que si su implementación se ha tomado la molestia de proporcionar las funciones pthreads, entonces se tomará la molestia de definir un modelo de memoria viable en el que los bloqueos sincronicen realmente el acceso a la memoria.

Si el compilador puede inline la función y demostrar que no tiene acceso a do_shutdown, a continuación, se puede almacenar en caché do_shutdown incluso en un entorno multiproceso ? ¿Qué ocurre con una función no en línea en la misma unidad de compilación?

Si a todo esto - si el objeto no es volátil, y el compilador puede demostrar que este hilo no lo modifica (ya sea a través de su nombre o por medio de un puntero de alias), y si no hay barreras de memoria ocurrir, entonces puede reutilizar valores previos. Puede y habrá otras condiciones específicas de la implementación que a veces lo detienen, por supuesto.

[*], siempre que la aplicación conoce el mundial no se encuentra en algún dirección de hardware "especial" que requiere que siempre lee ir a través de caché a la memoria principal con el fin de ver los resultados de cualquier hardware op afecta a esa dirección. Pero poner un elemento global en cualquier ubicación, o hacer que su ubicación sea especial con DMA o lo que sea, requiere magia específica para la implementación. A falta de tal magia, la implementación, en principio, a veces puede saber esto.

+0

Estoy 99% seguro de que en algún lugar POSIX define el bloqueo y el desbloqueo de mutex como barreras de memoria completas, pero no puedo encontrarlo ahora. –

+0

pthreads impone una barrera de memoria en 'pthread_cond_wait()': http://stackoverflow.com/questions/3208060/does-guarding-a-variable-with-a-pthread-mutex-guarantee-its-also-not- en caché/3208140 # 3208140 –

+1

@Michael, @R ..: a la derecha, define qué funciones "sincroniza la memoria", pero no define qué significa realmente "sincronizar la memoria". Hay más sobre esto en el documento de Boehm, cualquier otra cosa que diga será básicamente mi lectura (probablemente imperfecta) de su investigación. No hay mucha controversia de lo que significa, significa lo que realmente significan las "barreras de la memoria" en el hardware conocido, más algún comportamiento complejo del compilador para garantizar que las barreras no sean subvertidas por ciertos tipos de reordenamiento. Simplemente no es Posix el que dice esto, es el deseo comprensible de los compiladores de escribir herramientas útiles. –

2

Dado que do_shutdown tiene un enlace externo, no hay forma de que el compilador sepa lo que le sucede en la llamada (a menos que tenga plena visibilidad de las funciones a las que se llama). Por lo tanto, tendría que volver a cargar el valor (volátil o no - la interconexión no tiene nada que ver con esto) después de la llamada.

Hasta donde sé, no hay nada directamente dicho sobre esto en la norma, excepto que la máquina abstracta (de subproceso único) que usa el estándar para definir el comportamiento de las expresiones indica que la variable debe leerse cuando se accede a ella una expresión. El estándar permite que la lectura de la variable se optimice solo si se puede demostrar que el comportamiento es "como si" se recargó. Y eso solo puede suceder si el compilador puede saber que el valor no fue modificado por la llamada a la función.

Además, no es que la biblioteca pthread hace que ciertas garantías sobre las barreras de memoria para diversas funciones, incluyendo pthread_cond_wait(): Does guarding a variable with a pthread mutex guarantee it's also not cached?

Ahora, si do_shutdown eran estáticas (sin enlace externo) y tiene varios hilos que utilizan esa variable estática definido en el mismo módulo (es decir, la dirección de la variable estática nunca se tomó para pasar a otro módulo), esa podría ser una historia diferente. por ejemplo, supongamos que tiene una sola función que usó dicha variable e inició varias instancias de subprocesos ejecutándose para esa función. En ese caso, una implementación de compilador que cumpla con los estándares podría almacenar el valor en caché a través de llamadas a funciones, ya que podría suponer que nada más podría modificar el valor (el modelo de máquina abstracta estándar no incluye subprocesamiento).

Por lo tanto, en ese caso, tendría que usar mecanismos para garantizar que el valor se recargó en la llamada. Tenga en cuenta que debido a las complejidades del hardware, la palabra clave volatile puede no ser adecuada para garantizar el correcto acceso a la memoria. Debe confiar en las API proporcionadas por pthreads o el sistema operativo para garantizar eso. (Como nota al margen, las versiones recientes de los compiladores de Microsoft documentan que volatile imponen barreras de memoria completas, pero he leído opiniones que indican que esto no es requerido por la norma).

+1

Dado que la pregunta es "cualquier compilador que cumpla con los estándares", quizás eso incluya uno en el que toda la biblioteca estándar y todas las dependencias estén vinculadas estáticamente, de modo que con optimizaciones de tiempo de enlace/todo el programa, ¿la llamada es completamente visible? Tal vez no es un escenario realista. –

+0

Eso es lo que dice el sentido común, desafortunadamente no está claro si los estándares lo aplican o garantizan de alguna manera. –

+0

Si el compilador tiene conocimiento completo del programa, podría almacenar en caché el valor a través de la llamada de función si determinaba que nada podría modificar el valor. Pero la biblioteca pthreads está diseñada para evitar el almacenamiento en caché de una llamada que podría modificar dicho valor (la biblioteca podría necesitar realizar algo no estándar/específico de la implementación para hacer esto, pero ese es el problema de la biblioteca para resolverlo). –

0

De mi propio trabajo, puedo decir que sí, el compilador puede almacenar valores en caché en pthread_mutex_lock/pthread_mutex_unlock. Pasé la mayor parte del fin de semana rastreando un error en un poco de código causado por un conjunto de asignaciones de punteros que se almacenaban en caché y no estaban disponibles para los hilos que los necesitaban. Como una prueba rápida, envolví las asignaciones en un bloqueo/desbloqueo de mutex, y los hilos aún no tenían acceso a los valores de puntero apropiados. Al mover las asignaciones del puntero &, el bloqueo mutex asociado a una función separada solucionó el problema.

+0

¿Podría mostrarnos el código relevante? – curiousguy

2

Las respuestas de las agitaciones manuales son incorrectas. Lamento ser duro.

No hay manera

El compilador podía saber que hipotéticamente pthread_cond_wait() no modifica do_shutdown.

Si usted cree de manera diferente, por favor, muestre la prueba: a ++ programa completo en C tal que un compilador que no está diseñado para MT podría deducir que pthread_cond_wait no modifica do_shutdown.

es absurdo, un compilador no puede entender lo que hacen pthread_ funciones, a menos que tenga incorporado de conocimiento de los hilos POSIX.

Cuestiones relacionadas