Como se demostró en esta respuesta que publiqué recientemente, parece estar confundido acerca de la utilidad (o falta de ella) volatile
en contextos de programación de subprocesos múltiples.
Tengo entendido lo siguiente: cada vez que una variable puede cambiarse fuera del flujo de control de un fragmento de código que accede a ella, esa variable debe declararse como tal volatile
. Los manejadores de señales, los registros de E / S y las variables modificadas por otro hilo constituyen situaciones de este tipo.
Entonces, si tienes un int global foo
, y foo
es leído por un hilo y configurado atómicamente por otro hilo (probablemente usando una instrucción de máquina apropiada), el hilo de lectura ve esta situación de la misma manera que ve una variable ajustada por un controlador de señal o modificado por una condición de hardware externo y, por foo
lo tanto, debe declararse volatile
(o, para situaciones de subprocesos múltiples, acceder con carga protegida por memoria, que probablemente sea una mejor solución).
¿Cómo y dónde me equivoco?
Respuestas:
El problema con
volatile
un contexto multiproceso es que no proporciona todas las garantías que necesitamos. Tiene algunas propiedades que necesitamos, pero no todas, por lo que no podemos confiarvolatile
solo .Sin embargo, las primitivas que tendríamos que usar para las propiedades restantes también proporcionan las que lo
volatile
hacen, por lo que es efectivamente innecesario.Para accesos seguros para subprocesos a datos compartidos, necesitamos una garantía de que:
volatile
variable como indicador para indicar si algunos datos están listos o no para ser leídos. En nuestro código, simplemente establecemos la bandera después de preparar los datos, para que todo se vea bien. Pero, ¿qué pasa si las instrucciones se reordenan para que la bandera se establezca primero ?volatile
garantiza el primer punto. También garantiza que no se produzca un reordenamiento entre diferentes lecturas / escrituras volátiles . Todosvolatile
los accesos a la memoria ocurrirán en el orden en que se especifican. Eso es todo lo que necesitamos para lo quevolatile
está destinado: manipular registros de E / S o hardware mapeado en memoria, pero no nos ayuda en el código multiproceso donde elvolatile
objeto a menudo solo se usa para sincronizar el acceso a datos no volátiles. Esos accesos todavía se pueden reordenar en relación con los quevolatile
están.La solución para evitar la reordenación es utilizar una barrera de memoria , que indica tanto al compilador como a la CPU que no se puede reordenar el acceso a la memoria en este punto . La colocación de tales barreras alrededor de nuestro acceso variable volátil garantiza que incluso los accesos no volátiles no se reordenarán a través del volátil, lo que nos permite escribir código seguro para subprocesos.
Sin embargo, las barreras de memoria también aseguran que todas las lecturas / escrituras pendientes se ejecutan cuando se alcanza la barrera, por lo que efectivamente nos da todo lo que necesitamos por sí mismo, haciendo
volatile
innecesario. Simplemente podemos eliminar elvolatile
calificador por completo.Desde C ++ 11, las variables atómicas (
std::atomic<T>
) nos dan todas las garantías relevantes.fuente
volatile
no son lo suficientemente fuertes como para ser útiles.volatile
para ser una barrera de memoria completa (que evita la reordenación). Eso no es parte del estándar, por lo que no puede confiar en este comportamiento en código portátil.volatile
siempre es inútil para la programación de subprocesos múltiples. (Excepto en Visual Studio, donde la extensión de barrera de memoria es volátil .)También puede considerar esto de la documentación del kernel de Linux .
fuente
volatile
tiene sentido. En todos los casos, el comportamiento de "llamar a una función cuyo cuerpo no se puede ver" será correcto.No creo que se equivoque: la volatilidad es necesaria para garantizar que el subproceso A verá un cambio en el valor, si el valor se cambia por algo diferente al subproceso A. Según tengo entendido, la volátil es básicamente una forma de decirle al compilador "no guarde en caché esta variable en un registro, asegúrese de leerla / escribirla siempre desde la memoria RAM en cada acceso".
La confusión se debe a que la volatilidad no es suficiente para implementar una serie de cosas. En particular, los sistemas modernos utilizan múltiples niveles de almacenamiento en caché, las CPU multinúcleos modernas realizan algunas optimizaciones sofisticadas en tiempo de ejecución, y los compiladores modernos hacen algunas optimizaciones sofisticadas en tiempo de compilación, y todo esto puede dar lugar a varios efectos secundarios que se muestran en un diferente orden del orden que esperaría si solo mirara el código fuente.
Por lo tanto, volátil está bien, siempre y cuando tenga en cuenta que los cambios 'observados' en la variable volátil pueden no ocurrir en el momento exacto en que cree que ocurrirán. Específicamente, no intente usar variables volátiles como una forma de sincronizar u ordenar las operaciones entre hilos, porque no funcionará de manera confiable.
Personalmente, mi uso principal (¿solo?) Para la bandera volátil es como un booleano "pleaseGoAwayNow". Si tengo un subproceso de trabajo que realiza un bucle continuo, haré que compruebe el booleano volátil en cada iteración del bucle y salga si el booleano es cierto. El subproceso principal puede limpiar de forma segura el subproceso de trabajo estableciendo el valor booleano en verdadero y luego llamando a pthread_join () para esperar hasta que el subproceso de trabajo desaparezca.
fuente
mutex_lock
(y cualquier otra función de biblioteca) puede alterar el estado de la variable de bandera.SCHED_FIFO
, una prioridad estática más alta que otros procesos / subprocesos en el sistema, suficientes núcleos, deberían ser perfectamente posibles. En Linux puede especificar que el proceso en tiempo real pueda usar el 100% del tiempo de la CPU. Nunca cambiarán de contexto si no hay un subproceso / proceso de mayor prioridad y nunca bloquearán por E / S. Pero el punto es que C / C ++volatile
no está destinado a hacer cumplir la semántica adecuada de intercambio / sincronización de datos. La búsqueda de casos especiales para demostrar que un código incorrecto que a veces podría funcionar es un ejercicio inútil.volatile
es útil (aunque insuficiente) para implementar la construcción básica de un mutex spinlock, pero una vez que tiene eso (o algo superior), no necesita otrovolatile
.La forma típica de programación multiproceso no es proteger todas las variables compartidas a nivel de máquina, sino introducir variables de protección que guíen el flujo del programa. En lugar de
volatile bool my_shared_flag;
ti deberías haberEsto no solo encapsula la "parte difícil", es fundamentalmente necesario: C no incluye las operaciones atómicas necesarias para implementar un mutex; solo tiene
volatile
que hacer garantías adicionales sobre lo ordinario operaciones .Ahora tienes algo como esto:
my_shared_flag
no necesita ser volátil, a pesar de no ser almacenable en caché, porque&
operador).pthread_mutex_lock
Es una función de biblioteca.pthread_mutex_lock
alguna manera adquiere esa referencia.pthread_mutex_lock
modifica la bandera compartida !volatile
, aunque significativo en este contexto, es extraño.fuente
Tu comprensión realmente está mal.
La propiedad que tienen las variables volátiles es "las lecturas y las escrituras en esta variable son parte del comportamiento perceptible del programa". Eso significa que este programa funciona (dado el hardware apropiado):
El problema es que esta no es la propiedad que queremos de cualquier cosa segura para subprocesos.
Por ejemplo, un contador seguro para subprocesos sería solo (código similar al kernel de Linux, no sé el equivalente de c ++ 0x):
Esto es atómico, sin una barrera de memoria. Debe agregarlos si es necesario. Agregar volátil probablemente no ayudaría, ya que no relacionaría el acceso al código cercano (por ejemplo, a la adición de un elemento a la lista que el contador está contando). Ciertamente, no necesita ver el contador incrementado fuera de su programa, y las optimizaciones siguen siendo deseables, por ejemplo.
todavía se puede optimizar para
si el optimizador es lo suficientemente inteligente (no cambia la semántica del código).
fuente
Para que sus datos sean consistentes en un entorno concurrente, necesita dos condiciones para aplicar:
1) Atomicidad, es decir, si leo o escribo algunos datos en la memoria, esos datos se leen / escriben en una sola pasada y no se pueden interrumpir o contener debido, por ejemplo, a un cambio de contexto
2) Consistencia, es decir, el orden de las operaciones de lectura / escritura debe verse igual entre múltiples entornos concurrentes, ya sea hilos, máquinas, etc.
volátil no se ajusta a ninguno de los anteriores, o más particularmente, el estándar c o c ++ en cuanto a cómo debe comportarse el volátil no incluye ninguno de los anteriores.
Es aún peor en la práctica, ya que algunos compiladores (como el compilador Intel Itanium) intentan implementar algún elemento de comportamiento seguro de acceso concurrente (es decir, asegurando vallas de memoria), sin embargo, no hay coherencia entre las implementaciones del compilador y, además, el estándar no requiere esto de la implementación en primer lugar.
Marcar una variable como volátil solo significará que está obligando a que el valor se vacíe hacia y desde la memoria cada vez, lo que en muchos casos solo ralentiza su código, ya que básicamente ha reducido el rendimiento de su caché.
c # y java AFAIK corrigen esto haciendo que los volátiles se adhieran a 1) y 2), sin embargo, no se puede decir lo mismo de los compiladores de c / c ++, así que básicamente hazlo como mejor te parezca.
Para una discusión más profunda (aunque no imparcial) sobre el tema, lea esto
fuente
Las preguntas frecuentes de comp.programming.threads tienen una explicación clásica de Dave Butenhof:
Butenhof cubre gran parte del mismo terreno en esta publicación de Usenet :
Todo eso es igualmente aplicable a C ++.
fuente
Esto es todo lo que está haciendo "volátil": "Hey compilador, esta variable podría cambiar EN CUALQUIER MOMENTO (en cualquier marca de reloj), incluso si NO HAY INSTRUCCIONES LOCALES que actúen sobre ella. NO guarde en caché este valor en un registro".
Eso es. Le dice al compilador que su valor es, bueno, volátil; este valor puede ser alterado en cualquier momento por una lógica externa (otro hilo, otro proceso, el Kernel, etc.). Existe más o menos únicamente para suprimir las optimizaciones del compilador que guardarán en caché un valor en un registro que es inherentemente inseguro para NUNCA.
Puede encontrar artículos como "Dr. Dobbs" que se vuelven volátiles como una panacea para la programación de subprocesos múltiples. Su enfoque no carece totalmente de mérito, pero tiene la falla fundamental de hacer que los usuarios de un objeto sean responsables de su seguridad de subprocesos, que tiende a tener los mismos problemas que otras violaciones de encapsulación.
fuente
Según mi antiguo estándar C, "lo que constituye un acceso a un objeto que tiene un tipo calificado volátil está definido por la implementación" . Por lo tanto, los escritores de compiladores de C podrían haber elegido tener un " acceso seguro a subprocesos" en un entorno multiproceso "" volátil " . Pero no lo hicieron.
En cambio, las operaciones requeridas para hacer que un hilo de sección crítica sea seguro en un entorno de memoria compartida multiproceso multiproceso se agregaron como nuevas características definidas por la implementación. Y, liberados del requisito de que "volátil" proporcionaría acceso atómico y orden de acceso en un entorno multiproceso, los redactores del compilador priorizaron la reducción del código sobre la semántica "volátil" dependiente de la implementación histórica.
Esto significa que cosas como semáforos "volátiles" en torno a secciones de código críticas, que no funcionan en hardware nuevo con compiladores nuevos, podrían haber funcionado alguna vez con compiladores antiguos en hardware antiguo, y los ejemplos antiguos a veces no son incorrectos, simplemente antiguos.
fuente
volatile
que debería hacer para permitir que uno escriba un sistema operativo de una manera que depende del hardware pero del compilador. Exigir que los programadores utilicen funciones dependientes de la implementación en lugar de hacer que elvolatile
trabajo sea necesario socava el propósito de tener un estándar.