c++ - ejecutar - Cómo el compilador como GCC implementa la semántica de adquisición/lanzamiento para std:: mutex
compilar en cmd con gcc (4)
Tengo entendido que std :: mutex lock and unlock tiene una semántica de adquisición / liberación que evitará que las instrucciones entre ellos se muevan hacia afuera.
Por lo tanto, adquirir / liberar debe deshabilitar las instrucciones de compilación y reordenación de la CPU.
Mi pregunta es que miro la base de códigos GCC5.1 y no veo nada especial en std :: mutex :: lock / unlock para evitar que los códigos de reordenación del compilador.
Encuentro una respuesta potencial en does-pthread-mutex-lock-have-happens-before-semantics que indica que un mail que dice que una llamada de función externa actúa como cercas de memoria del compilador.
¿Siempre es verdad? ¿Y dónde está el estándar?
Por lo tanto, adquirir / liberar debe deshabilitar las instrucciones de compilación y reordenación de la CPU.
Por definición, todo lo que impide la reordenación de la CPU mediante la ejecución especulativa impide la reordenación del compilador. Esa es la definición de semántica del lenguaje, incluso sin MT (subprocesos múltiples) en el idioma, por lo que estará seguro de reordenar en compiladores antiguos que no admiten MT.
Pero estos compiladores no son seguros para MT por varias razones, desde la falta de protección de subprocesos alrededor de la inicialización de las variables estáticas en tiempo de ejecución hasta las variables globales modificadas implícitamente como errno, etc.
Además, en C / C ++, cualquier llamada a una función que sea puramente externa (es decir, no en línea, disponible para integrarse en cualquier punto), sin anotación que explique lo que hace (como el atributo "función pura" de algún compilador popular) , se debe asumir que hace todo lo que puede hacer el código C / C ++ legal. No sería posible un reordenamiento no trivial (cualquier reordenamiento que sea visible no es trivial).
Cualquier implementación correcta de bloqueos en sistemas con múltiples unidades de ejecución que no simulen un orden global en las instrucciones de ensamblaje requerirá barreras de memoria y evitará que se vuelvan a ordenar.
Una implementación de bloqueos en una CPU de ejecución lineal, con solo una unidad de ejecución (o donde todos los subprocesos están vinculados en la misma unidad de ejecución), puede usar solo variables volátiles para la sincronización y eso no es seguro como las lecturas volátiles Los escritos no proporcionan ninguna garantía de adquisición resp. Liberación de cualquier otro dato (contraste de Java). Se necesitaría algún tipo de barrera del compilador, como una llamada de función fuertemente externa, o algún asm (""/*nothing*/)
(que es específico del compilador e incluso específico de la versión del compilador).
Los hilos son una característica bastante complicada, de bajo nivel. Históricamente, no había una funcionalidad estándar de subprocesos en C, y en su lugar se hacía de manera diferente en diferentes sistemas operativos. Hoy en día, existe principalmente el estándar de subprocesos POSIX, que se ha implementado en Linux y BSD, y ahora con la extensión OS X, y hay subprocesos de Windows, que comienzan con Win32 y así sucesivamente. Potencialmente, podría haber otros sistemas además de estos.
GCC no contiene directamente una implementación de subprocesos POSIX, en su lugar puede ser un cliente de libpthread
en un sistema Linux. Cuando creas GCC desde la fuente, tienes que configurar y construir por separado una cantidad de bibliotecas auxiliares, que admiten cosas como grandes números y subprocesos. Ese es el punto en el que selecciona cómo se realizará el subprocesamiento. Si lo hace de forma estándar en linux, tendrá una implementación de std::thread
en términos de pthreads.
En Windows, comenzando con el cumplimiento de MSVC C ++ 11, los desarrolladores de MSVC implementaron std::thread
en términos de la interfaz de subprocesos nativos de Windows.
El trabajo del sistema operativo es garantizar que los bloqueos de concurrencia proporcionados por su API realmente funcionen: std::thread
debe ser una interfaz multiplataforma para una primitiva de este tipo.
La situación puede ser más complicada para plataformas más exóticas / compilación cruzada, etc. Por ejemplo, en el proyecto MinGW (gcc para ventanas), históricamente, tiene la opción de construir MinGW gcc usando un puerto de pthreads para ventanas o usando un modelo de subprocesamiento basado en win32 nativo. Si no configura esto cuando construye, puede terminar con un compilador de C ++ 11 que no admita std::thread
o std::mutex
. Vea esta pregunta para más detalles. Error de MinGW: ''thread'' no es miembro de ''std''
Ahora, para responder a su pregunta más directamente. Cuando un mutex está activado, en el nivel más bajo, esto implica alguna llamada a libpthreads o alguna API win32.
pthread_lock_mutex();
do_some_stuff();
pthread_unlock_mutex();
( pthread_lock_mutex
y pthread_unlock_mutex
corresponden a las implementaciones de lock
y unlock
de std::mutex
en su plataforma, y en el código idiomático C ++ 11, estos se llaman a su vez en el ctor
y dtor
de std::unique_lock
por ejemplo, si usted están usando eso.)
En general, el optimizador no puede reordenar esto a menos que esté seguro de que pthread_lock_mutex()
no tiene efectos secundarios que puedan cambiar el comportamiento observable de do_some_stuff()
.
Que yo sepa, el mecanismo que tiene el compilador para hacer esto es, en última instancia, el mismo que utiliza para estimar los posibles efectos secundarios de las llamadas a cualquier otra biblioteca externa.
Si hay algún recurso.
int resource;
que está en disputa entre varios hilos, significa que hay algún cuerpo de función
void compete_for_resource();
y un puntero a esta función se encuentra en algún punto anterior pasado a pthread_create...
en su programa para iniciar otro hilo. (Esto presumiblemente estaría en la implementación del ctor
de std::thread
.) En este punto, el compilador puede ver que cualquier llamada a libpthread
puede potencialmente llamar compete_for_resource
y tocar cualquier memoria que toque esa función. (Desde el punto de vista del compilador, libpthread
es una caja negra, es un libpthread
.dll
/ .so
y no puede hacer suposiciones sobre lo que hace exactamente).
En particular, la llamada pthread_lock_mutex();
potencialmente tiene efectos secundarios para el resource
, por lo que no se puede reordenar contra do_some_stuff()
.
Si nunca generaste ningún otro hilo, entonces, que yo sepa, do_some_stuff();
Podría ser reordenado fuera del bloqueo mutex. Desde entonces, libpthread
no tiene ningún acceso a resource
, es solo una variable privada en su fuente y no se comparte con la biblioteca externa ni siquiera de manera indirecta, y el compilador puede ver eso.
NOTA: No soy un experto en esta área y mi conocimiento sobre esto está en una condición de espagueti. Así que toma la respuesta con un grano de sal.
NOTA-2: Esta podría no ser la respuesta que espera OP. Pero aquí están mis 2 centavos de todos modos si ayuda:
Mi pregunta es que miro la base de códigos GCC5.1 y no veo nada especial en std :: mutex :: lock / unlock para evitar que los códigos de reordenación del compilador.
g ++ utilizando la librería pthread. std :: mutex es solo una envoltura delgada alrededor de pthread_mutex
. Por lo tanto, tendrá que ir y echar un vistazo a la implementación de mutex de pthread.
Si profundiza un poco en la implementación de pthread (que puede encontrar here ), verá que utiliza instrucciones atómicas junto con llamadas de futex
.
Dos cosas menores para recordar aquí:
1. Las instrucciones atómicas hacen uso de barreras.
2. Cualquier llamada de función es equivalente a la barrera completa. No recuerdo de donde lo leí.
3. las llamadas mutex
pueden poner el hilo en suspensión y provocar un cambio de contexto.
Ahora, en lo que respecta a la reordenación, una de las cosas que debe garantizarse es que, después del lock
y sin unlock
deben reordenar las instrucciones antes del lock
o después del unlock
. Creo que esto no es una barrera completa, sino más bien una barrera de adquisición y liberación respectivamente. Pero, de nuevo, esto depende de la plataforma, x86 proporciona consistencia secuencial de forma predeterminada, mientras que ARM proporciona una garantía de pedido más débil.
Recomiendo esta serie de blogs: http://preshing.com/archives/ Explica muchas cosas de nivel inferior en un lenguaje fácil de entender. Adivina, tengo que leerlo una vez más :)
ACTUALIZACIÓN :: No se puede comentar en la respuesta de @Cort Ammons debido a la longitud
@ Kane No estoy seguro de esto, pero la gente en general escribe barreras para el nivel del procesador que también se ocupa de las barreras del nivel del compilador. Lo mismo no es cierto para las barreras integradas del compilador.
Ahora, debido a que las definiciones de las funciones pthread_*lock*
no están presentes en la unidad de traducción en la que lo está utilizando (esto es dudoso), el bloqueo de llamadas debe proporcionarle una barrera de memoria completa. La implementación pthread para la plataforma hace uso de instrucciones atómicas para bloquear cualquier otro hilo para que no acceda a las ubicaciones de la memoria después del bloqueo o antes del desbloqueo. Ahora, ya que solo un hilo está ejecutando la parte crítica del código, se garantiza que cualquier reordenamiento dentro de eso no cambiará el comportamiento esperado como se menciona en el comentario anterior.
La atómica es bastante difícil de entender y de hacer bien, así que lo que he escrito arriba es de mi comprensión. Estaría muy contento de saber si mi entendimiento está equivocado aquí.
Todas estas preguntas se derivan de las reglas para la reordenación del compilador. Una de las reglas fundamentales para reordenar es que el compilador debe probar que el reordenar no cambia el resultado del programa. En el caso de std::mutex
, el significado exacto de esa frase se especifica en un bloque de aproximadamente 10 páginas de legaleés, pero el sentido general intuitivo de "no cambia el resultado del programa" se mantiene. Si tenía una garantía sobre qué operación se realizó primero, de acuerdo con la especificación, ningún compilador tiene permitido reordenar de una manera que viole esa garantía.
Esta es la razón por la que la gente suele afirmar que una "llamada de función actúa como una barrera de memoria". Si el compilador no puede inspeccionar a fondo la función, no puede probar que la función no tenía una barrera oculta o una operación atómica dentro de ella, por lo que debe tratar esa función como si fuera una barrera.
Existe, por supuesto, el caso en el que el compilador puede inspeccionar la función, como el caso de las funciones en línea o las optimizaciones de tiempo de enlace. En estos casos, uno no puede confiar en una llamada a la función para que actúe como una barrera, porque el compilador de hecho puede tener suficiente información para probar que la reescritura se comporta igual que el original.
En el caso de mutexes, incluso tal optimización avanzada no puede llevarse a cabo. La única forma de reordenar las llamadas de función de bloqueo / desbloqueo de mutex es hacer una inspección profunda de las funciones y comprobar que no existen barreras ni operaciones atómicas con las que lidiar. Si no puede inspeccionar cada sub llamada y sub llamada de esa función de bloqueo / desbloqueo, no puede probar que es seguro reordenar. Si realmente puede hacer esta inspección, vería que cada implementación de exclusión mutua contiene algo que no se puede reordenar (de hecho, esto es parte de la definición de una implementación de exclusión mutua válida). Por lo tanto, incluso en ese caso extremo, el compilador todavía tiene prohibido optimizar.
EDITAR : Para completar, me gustaría señalar que estas reglas se introdujeron en C ++ 11. Las reglas de reordenamiento de C ++ 98 y C ++ 03 solo prohibieron cambios que afectaron el resultado del hilo actual . Tal garantía no es lo suficientemente fuerte como para desarrollar primitivas multihilo como mutexes.
Para lidiar con esto, las APIs de subprocesos múltiples como pthreads desarrollaron sus propias reglas. De la sección de especificación de Pthreads 4.11 :
Las aplicaciones deben garantizar que el acceso a cualquier ubicación de la memoria por más de un subproceso de control (subprocesos o procesos) esté restringido de modo que ningún subproceso de control pueda leer o modificar una ubicación de la memoria mientras que otro subproceso de control puede estar modificándolo. Dicho acceso está restringido usando funciones que sincronizan la ejecución de subprocesos y también sincronizan la memoria con respecto a otros subprocesos. Las siguientes funciones sincronizan la memoria con respecto a otros hilos.
Luego, enumera una docena de funciones que sincronizan la memoria, incluyendo pthread_mutex_lock
y pthread_mutex_unlock
.
Un compilador que desee admitir la biblioteca pthreads debe implementar algo para admitir esta sincronización de memoria entre subprocesos, aunque la especificación de C ++ no haya dicho nada al respecto. Afortunadamente, cualquier compilador en el que desee realizar subprocesos múltiples se desarrolló con el reconocimiento de que tales garantías son fundamentales para todos los subprocesos múltiples, por lo que cada compilador que admite subprocesos múltiples lo tiene.
En el caso de gcc, lo hizo sin ninguna nota especial en las llamadas a la función pthreads porque gcc crearía efectivamente una barrera alrededor de cada llamada a una función externa (porque no pudo probar que no haya sincronización dentro de esa llamada a la función). Si gcc cambiara eso alguna vez, también tendrían que cambiar sus encabezados pthreads para incluir cualquier palabra extra necesaria para marcar las funciones pthreads como memoria de sincronización.
Todo eso, por supuesto, es específico del compilador. No hubo respuestas estándar para esta pregunta hasta que C ++ 11 vino junto con su nuevo modelo de memoria.