¿Por qué el volátil no se considera útil en la programación multiproceso C o C ++?

165

Como se demostró en esta respuesta que publiqué recientemente, parece estar confundido acerca de la utilidad (o falta de ella) volatileen 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 fooes 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 foolo 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?

Michael Ekstrand
fuente
77
Todo lo que hace volátil es decir que el compilador no debe almacenar en caché el acceso a una variable volátil. No dice nada acerca de serializar dicho acceso. Esto se ha discutido aquí. No sé cuántas veces, y no creo que esta pregunta vaya a agregar nada a esas discusiones.
44
Y una vez más, una pregunta que no lo merece, y que se ha formulado aquí muchas veces antes, recibe un voto positivo. ¿Podrías dejar de hacer esto?
14
@neil Busqué otras preguntas y encontré una, pero cualquier explicación existente que vi de alguna manera no desencadenó lo que necesitaba para entender realmente por qué estaba equivocado. Esta pregunta ha suscitado tal respuesta.
Michael Ekstrand
1
Para un gran estudio en profundidad sobre lo que hacen las CPU con los datos (a través de sus cachés), visite: rdrop.com/users/paulmck/scalability/paper/whymb.2010.06.07c.pdf
Sassafras_wot
1
@curiousguy Eso es lo que quise decir con "no es el caso en C", donde se puede usar para escribir en registros de hardware, etc., y no se usa para subprocesos múltiples como se usa comúnmente en Java.
Monstieur

Respuestas:

213

El problema con volatileun 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 confiar volatile solo .

Sin embargo, las primitivas que tendríamos que usar para las propiedades restantes también proporcionan las que lo volatilehacen, por lo que es efectivamente innecesario.

Para accesos seguros para subprocesos a datos compartidos, necesitamos una garantía de que:

  • la lectura / escritura realmente sucede (que el compilador no solo almacenará el valor en un registro y aplazará la actualización de la memoria principal hasta mucho más tarde)
  • que no se reordena Supongamos que usamos una volatilevariable 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 ?

volatilegarantiza el primer punto. También garantiza que no se produzca un reordenamiento entre diferentes lecturas / escrituras volátiles . Todos volatilelos accesos a la memoria ocurrirán en el orden en que se especifican. Eso es todo lo que necesitamos para lo que volatileestá destinado: manipular registros de E / S o hardware mapeado en memoria, pero no nos ayuda en el código multiproceso donde el volatileobjeto 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 que volatileestá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 volatileinnecesario. Simplemente podemos eliminar el volatilecalificador por completo.

Desde C ++ 11, las variables atómicas ( std::atomic<T>) nos dan todas las garantías relevantes.

jalf
fuente
55
@jbcreix: ¿De qué "eso" estás preguntando? ¿Barreras volátiles o de memoria? En cualquier caso, la respuesta es más o menos la misma. Ambos tienen que trabajar tanto a nivel de compilador como de CPU, ya que describen el comportamiento observable del programa, por lo que deben asegurarse de que la CPU no reordene todo, cambiando el comportamiento que garantizan. Pero actualmente no puede escribir sincronización de subprocesos portátil, porque las barreras de memoria no son parte de C ++ estándar (por lo que no son portátiles) y volatileno son lo suficientemente fuertes como para ser útiles.
jalf
44
Un ejemplo de MSDN hace esto y afirma que las instrucciones no se pueden reordenar después de un acceso volátil: msdn.microsoft.com/en-us/library/12a04hfd(v=vs.80).aspx
OJW
27
@OJW: Pero el compilador de Microsoft redefine 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.
jalf
44
@Skizz: no, ahí es donde entra la parte "mágica del compilador" de la ecuación. Tanto la CPU como el compilador deben comprender una barrera de memoria . Si el compilador comprende la semántica de una barrera de memoria, sabe evitar trucos como ese (así como reordenar lecturas / escrituras a través de la barrera). Y afortunadamente, el compilador hace entender la semántica de una barrera de memoria, así que al final, todo se resuelve. :)
jalf
13
@Skizz: los subprocesos siempre son una extensión dependiente de la plataforma antes de C ++ 11 y C11. Que yo sepa, cada entorno C y C ++ que proporciona una extensión de subprocesos también proporciona una extensión de "barrera de memoria". De todos modos, volatilesiempre 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 .)
Nemo
49

También puede considerar esto de la documentación del kernel de Linux .

Los programadores de C a menudo han tomado volátil para significar que la variable podría cambiarse fuera del hilo actual de ejecución; Como resultado, a veces se sienten tentados a usarlo en el código del núcleo cuando se utilizan estructuras de datos compartidos. En otras palabras, se sabe que tratan los tipos volátiles como una especie de variable atómica fácil, que no lo son. El uso de volátiles en el código del núcleo casi nunca es correcto; Este documento describe por qué.

El punto clave a entender con respecto a los volátiles es que su propósito es suprimir la optimización, que casi nunca es lo que uno realmente quiere hacer. En el núcleo, uno debe proteger las estructuras de datos compartidos contra el acceso concurrente no deseado, que es una tarea muy diferente. El proceso de protección contra la concurrencia no deseada también evitará casi todos los problemas relacionados con la optimización de una manera más eficiente.

Al igual que los volátiles, las primitivas del kernel que hacen que el acceso concurrente a los datos sea seguro (spinlocks, mutexes, barreras de memoria, etc.) están diseñados para evitar la optimización no deseada. Si se usan correctamente, no será necesario usar volátiles también. Si todavía es necesario volátil, es casi seguro que haya un error en el código en alguna parte. En el código de kernel escrito correctamente, volátil solo puede servir para ralentizar las cosas.

Considere un bloque típico de código del núcleo:

spin_lock(&the_lock);
do_something_on(&shared_data);
do_something_else_with(&shared_data);
spin_unlock(&the_lock);

Si todo el código sigue las reglas de bloqueo, el valor de shared_data no puede cambiar inesperadamente mientras se mantiene the_lock. Cualquier otro código que quiera jugar con esos datos estará esperando en la cerradura. Las primitivas de spinlock actúan como barreras de memoria, están escritas explícitamente para hacerlo, lo que significa que los accesos a datos no se optimizarán en ellas. Por lo tanto, el compilador podría pensar que sabe lo que estará en shared_data, pero la llamada spin_lock (), ya que actúa como una barrera de memoria, lo obligará a olvidar todo lo que sabe. No habrá problemas de optimización con los accesos a esos datos.

Si shared_data se declarara volátil, el bloqueo seguiría siendo necesario. Pero también se evitaría que el compilador optimice el acceso a shared_data dentro de la sección crítica, cuando sabemos que nadie más puede trabajar con él. Mientras se mantiene el bloqueo, shared_data no es volátil. Cuando se trata de datos compartidos, el bloqueo adecuado hace que la volatilidad sea innecesaria y potencialmente dañina.

La clase de almacenamiento volátil se diseñó originalmente para registros de E / S mapeados en memoria. Dentro del núcleo, los accesos de registro también deben estar protegidos por bloqueos, pero tampoco se quiere que el compilador "optimice" los accesos de registro dentro de una sección crítica. Pero, dentro del núcleo, los accesos a la memoria de E / S siempre se realizan a través de funciones de acceso; acceder a la memoria de E / S directamente a través de punteros está mal visto y no funciona en todas las arquitecturas. Esos accesorios están escritos para evitar la optimización no deseada, por lo que, una vez más, la volatilidad es innecesaria.

Otra situación en la que uno podría verse tentado a usar volátiles es cuando el procesador está ocupado esperando el valor de una variable. La forma correcta de realizar una espera ocupada es:

while (my_variable != what_i_want)
    cpu_relax();

La llamada cpu_relax () puede reducir el consumo de energía de la CPU o ceder el paso a un procesador gemelo con hiperprocesamiento; También sirve como barrera de memoria, por lo que, una vez más, lo volátil es innecesario. Por supuesto, la espera ocupada es generalmente un acto antisocial para empezar.

Todavía hay algunas situaciones raras en las que volátil tiene sentido en el núcleo:

  • Las funciones de acceso mencionadas anteriormente pueden usar volátiles en arquitecturas donde el acceso directo a la memoria de E / S sí funciona. Esencialmente, cada llamada de acceso se convierte en una pequeña sección crítica por sí misma y asegura que el acceso se realice como lo espera el programador.

  • El código de ensamblaje en línea que cambia la memoria, pero que no tiene otros efectos secundarios visibles, corre el riesgo de ser eliminado por GCC. Agregar la palabra clave volátil a las declaraciones asm evitará esta eliminación.

  • La variable jiffies es especial porque puede tener un valor diferente cada vez que se hace referencia, pero se puede leer sin ningún bloqueo especial. Por lo tanto, los jiffies pueden ser volátiles, pero la adición de otras variables de este tipo está muy mal vista. Jiffies se considera un tema de "legado estúpido" (palabras de Linus) a este respecto; arreglarlo sería más problemático de lo que vale.

  • Los punteros a estructuras de datos en memoria coherente que pueden ser modificados por dispositivos de E / S pueden, a veces, legítimamente ser volátiles. Un buffer de anillo utilizado por un adaptador de red, donde ese adaptador cambia los punteros para indicar qué descriptores se han procesado, es un ejemplo de este tipo de situación.

Para la mayoría de los códigos, no se aplica ninguna de las justificaciones anteriores para volátiles. Como resultado, es probable que el uso de volátiles se vea como un error y traerá un escrutinio adicional al código. Los desarrolladores que se sienten tentados a usar volátiles deben dar un paso atrás y pensar en lo que realmente están tratando de lograr.

Comunidad
fuente
3
@ curiousguy: sí. Ver también gcc.gnu.org/onlinedocs/gcc-4.0.4/gcc/Extended-Asm.html .
Sebastian Mach
1
El spin_lock () se parece a una llamada de función normal. Lo que es especial es que el compilador lo tratará especialmente para que el código generado "olvide" cualquier valor de shared_data que se haya leído antes del spin_lock () y se haya almacenado en un registro para que el valor tenga que leerse nuevamente en el do_something_on () después del spin_lock ()?
Sincopado el
1
@underscore_d Mi punto es que no puedo decir por el nombre de la función spin_lock () que hace algo especial. No sé lo que hay dentro. Particularmente, no sé qué hay en la implementación que impide que el compilador optimice las lecturas posteriores.
Sincopado el
1
Sincopado tiene un buen punto. Esto esencialmente significa que el programador debe conocer la implementación interna de esas "funciones especiales" o al menos estar muy bien informado sobre su comportamiento. Esto plantea preguntas adicionales, tales como: ¿estas funciones especiales están estandarizadas y garantizadas para funcionar de la misma manera en todas las arquitecturas y todos los compiladores? ¿Existe una lista de tales funciones disponibles o al menos existe una convención para usar comentarios de código para indicar a los desarrolladores que la función en cuestión protege el código contra la "optimización"?
JustAMartin
1
@Tuntable: cualquier código puede tocar una estática privada mediante un puntero. Y se está tomando su dirección. Quizás el análisis de flujo de datos sea capaz de demostrar que el puntero nunca escapa, pero eso es en general un problema muy difícil, superlineal en el tamaño del programa. Si tiene una forma de garantizar que no existan alias, entonces mover el acceso a través de un bloqueo giratorio debería estar bien. Pero si no existen alias, tampoco volatiletiene sentido. En todos los casos, el comportamiento de "llamar a una función cuyo cuerpo no se puede ver" será correcto.
Ben Voigt
11

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.

Jeremy Friesner
fuente
2
Su bandera booleana es probablemente insegura. ¿Cómo garantiza que el trabajador complete su tarea y que el indicador permanezca dentro del alcance hasta que se lea (si se lee)? Ese es un trabajo para las señales. Volátil es bueno para implementar spinlocks simples si no hay mutex involucrado, ya que la seguridad de alias significa que el compilador asume mutex_lock(y cualquier otra función de biblioteca) puede alterar el estado de la variable de bandera.
Potatoswatter
66
Obviamente, solo funciona si la naturaleza de la rutina del subproceso de trabajo es tal que se garantiza que verifique el booleano periódicamente. Se garantiza que el indicador volatile-bool permanecerá dentro del alcance porque la secuencia de cierre de subproceso siempre se produce antes de que el objeto que contiene el booleano volátil se destruya, y la secuencia de cierre de subproceso llama a pthread_join () después de establecer el bool. pthread_join () se bloqueará hasta que el subproceso de trabajo desaparezca. Las señales tienen sus propios problemas, especialmente cuando se usan junto con subprocesos múltiples.
Jeremy Friesner
2
No se garantiza que el subproceso de trabajo complete su trabajo antes de que el valor booleano sea verdadero; de hecho, es casi seguro que estará en el medio de una unidad de trabajo cuando el valor bool sea verdadero. Pero no importa cuándo el subproceso de trabajo completa su unidad de trabajo, porque el subproceso principal no hará nada excepto bloquear dentro de pthread_join () hasta que el subproceso de trabajo salga, en cualquier caso. Por lo tanto, la secuencia de apagado está bien ordenada: el bool volátil (y cualquier otro dato compartido) no se liberará hasta que pthread_join () regrese, y pthread_join () no volverá hasta que el subproceso de trabajo desaparezca.
Jeremy Friesner
10
@ Jeremy, tienes razón en la práctica, pero teóricamente aún podría romperse. En un sistema de dos núcleos, un núcleo ejecuta constantemente su hilo de trabajo. El otro núcleo establece el valor de bool en verdadero. Sin embargo, no hay garantía de que el núcleo del subproceso de trabajo verá ese cambio, es decir, nunca se detendrá a pesar de que verifique repetidamente el bool. Este comportamiento está permitido por los modelos de memoria c ++ 0x, java y c #. En la práctica, esto nunca ocurriría, ya que el hilo ocupado probablemente inserte una barrera de memoria en algún lugar, después de lo cual verá el cambio en el bool.
deft_code
44
Tome un sistema POSIX, use una política de programación en tiempo real 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 ++ volatileno 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.
FooF
7

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 otro volatile .

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 haber

pthread_mutex_t flag_guard_mutex; // contains something volatile
bool my_shared_flag;

Esto no solo encapsula la "parte difícil", es fundamentalmente necesario: C no incluye las operaciones atómicas necesarias para implementar un mutex; solo tiene volatileque hacer garantías adicionales sobre lo ordinario operaciones .

Ahora tienes algo como esto:

pthread_mutex_lock( &flag_guard_mutex );
my_local_state = my_shared_flag; // critical section
pthread_mutex_unlock( &flag_guard_mutex );

pthread_mutex_lock( &flag_guard_mutex ); // may alter my_shared_flag
my_shared_flag = ! my_shared_flag; // critical section
pthread_mutex_unlock( &flag_guard_mutex );

my_shared_flag no necesita ser volátil, a pesar de no ser almacenable en caché, porque

  1. Otro hilo tiene acceso a él.
  2. Es decir, una referencia a ella debe haberse tomado en algún momento (con el & operador).
    • (O se tomó una referencia a una estructura que contiene)
  3. pthread_mutex_lock Es una función de biblioteca.
  4. Lo que significa que el compilador no puede decir si de pthread_mutex_lockalguna manera adquiere esa referencia.
  5. ¡Lo que significa que el compilador debe asumir que pthread_mutex_lockmodifica la bandera compartida !
  6. Entonces la variable debe ser recargada desde la memoria. volatile, aunque significativo en este contexto, es extraño.
Agua de patata
fuente
6

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):

int volatile* reg=IO_MAPPED_REGISTER_ADDRESS;
*reg=1; // turn the fuel on
*reg=2; // ignition
*reg=3; // release
int x=*reg; // fire missiles

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):

atomic_t counter;

...
atomic_inc(&counter);

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.

atomic_inc(&counter);
atomic_inc(&counter);

todavía se puede optimizar para

atomically {
  counter+=2;
}

si el optimizador es lo suficientemente inteligente (no cambia la semántica del código).

jpalecek
fuente
6

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

zebrabox
fuente
3
+1: la atomicidad garantizada era otra pieza de lo que me faltaba. Estaba suponiendo que cargar un int es atómico, por lo que la volatilidad evitando el reordenamiento proporcionó la solución completa en el lado de lectura. Creo que es una suposición decente en la mayoría de las arquitecturas, pero no es una garantía.
Michael Ekstrand
¿Cuándo son las lecturas y escrituras individuales en memoria interrumpibles y no atómicas? ¿Hay algún beneficio?
Batbrat
5

Las preguntas frecuentes de comp.programming.threads tienen una explicación clásica de Dave Butenhof:

P56: ¿Por qué no necesito declarar VOLATILES las variables compartidas?

Sin embargo, me preocupan los casos en que tanto el compilador como la biblioteca de hilos cumplen con sus respectivas especificaciones. Un compilador de C conforme puede asignar globalmente alguna variable compartida (no volátil) a un registro que se guarda y restaura a medida que la CPU pasa de hilo a hilo. Cada hilo tendrá su propio valor privado para esta variable compartida, que no es lo que queremos de una variable compartida.

En cierto sentido, esto es cierto si el compilador conoce lo suficiente sobre los ámbitos respectivos de la variable y las funciones pthread_cond_wait (o pthread_mutex_lock). En la práctica, la mayoría de los compiladores no tratarán de mantener copias de registro de datos globales a través de una llamada a una función externa, porque es demasiado difícil saber si la rutina de alguna manera podría tener acceso a la dirección de los datos.

Entonces, sí, es cierto que un compilador que se ajusta estrictamente (pero de manera muy agresiva) a ANSI C podría no funcionar con múltiples subprocesos sin volátil. Pero es mejor que alguien lo arregle. Debido a que cualquier SISTEMA (es decir, pragmáticamente, una combinación de núcleo, bibliotecas y compilador de C) que no proporciona las garantías de coherencia de memoria POSIX no se CONFORME con el estándar POSIX. Período. El sistema NO PUEDE requerir que utilice variables volátiles en compartidas para un comportamiento correcto, porque POSIX solo requiere que las funciones de sincronización POSIX sean necesarias.

Entonces, si su programa se rompe porque no utilizó volátil, eso es un ERROR. Puede no ser un error en C, o un error en la biblioteca de hilos, o un error en el núcleo. Pero es un error del SISTEMA, y uno o más de esos componentes tendrán que trabajar para solucionarlo.

No desea utilizar volátiles, porque, en cualquier sistema en el que haga alguna diferencia, será mucho más costoso que una variable no volátil adecuada. (ANSI C requiere "puntos de secuencia" para las variables volátiles en cada expresión, mientras que POSIX los requiere solo en las operaciones de sincronización: una aplicación de subprocesos intensiva en cómputo verá sustancialmente más actividad de memoria usando volátiles y, después de todo, es la actividad de la memoria la que realmente te frena)

/ --- [Dave Butenhof] ----------------------- [[email protected]] --- \
| Digital Equipment Corporation 110 Spit Brook Rd ZKO2-3 / Q18 |
El | 603.881.2218, FAX 603.881.0120 Nashua NH 03062-2698 |
----------------- [Mejor vida a través de la concurrencia] ---------------- /

Butenhof cubre gran parte del mismo terreno en esta publicación de Usenet :

El uso de "volátil" no es suficiente para garantizar una visibilidad adecuada de la memoria o sincronización entre hilos. El uso de un mutex es suficiente y, excepto recurriendo a varias alternativas de código de máquina no portátil (o implicaciones más sutiles de las reglas de memoria POSIX que son mucho más difíciles de aplicar en general, como se explicó en mi publicación anterior), un mutex es NECESARIO.

Por lo tanto, como explicó Bryan, el uso de volátiles no logra más que evitar que el compilador realice optimizaciones útiles y deseables, sin proporcionar ayuda alguna para que el código sea "seguro para subprocesos". Usted es bienvenido, por supuesto, para declarar cualquier cosa que desee como "volátil": después de todo, es un atributo legal de almacenamiento ANSI C. Simplemente no esperes que resuelva ningún problema de sincronización de subprocesos por ti.

Todo eso es igualmente aplicable a C ++.

Tony Delroy
fuente
El enlace está roto; ya no parece apuntar a lo que querías citar. Sin el texto, es una especie de respuesta sin sentido.
jww
3

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.

Zack Yezek
fuente
3

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.

david
fuente
Los ejemplos anteriores requerían que el programa fuera procesado por compiladores de calidad adecuados para la programación de bajo nivel. Desafortunadamente, los compiladores "modernos" han tomado el hecho de que el Estándar no les exige que procesen "volátil" de manera útil como una indicación de que el código que requeriría que lo hagan está roto, en lugar de reconocer que el Estándar no esfuerzo para prohibir implementaciones que sean conformes pero de tan baja calidad que sean inútiles, pero que de ninguna manera aprueben los compiladores de baja calidad pero conformes que se han vuelto populares
supercat
En la mayoría de las plataformas, sería bastante fácil reconocer lo volatileque 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 el volatiletrabajo sea necesario socava el propósito de tener un estándar.
supercat