Pregunta

En el lenguaje de programación C y Pthreads como biblioteca de subprocesos;¿Las variables/estructuras que se comparten entre subprocesos deben declararse como volátiles?Suponiendo que puedan estar protegidos por una cerradura o no (quizás con barreras).

¿El estándar pthread POSIX tiene algo que decir al respecto? ¿Depende del compilador o ninguno de los dos?

Editar para agregar:Gracias por las buenas respuestas.¿Pero qué pasa si estás no usando cerraduras;¿Qué pasa si estás usando? barreras ¿Por ejemplo?O código que utiliza primitivas como comparar e intercambiar modificar directa y atómicamente una variable compartida...

¿Fue útil?

Solución

Creo que una propiedad muy importante de volátil es que hace que la variable se escriba en la memoria cuando se modifica y se vuelva a leer desde la memoria cada vez que se accede.Las otras respuestas aquí combinan volátil y sincronización, y de algunas otras respuestas además de esta se desprende claramente que volátil NO es una primitiva de sincronización (crédito a quien se debe el crédito).

Pero a menos que use volátil, el compilador es libre de almacenar en caché los datos compartidos en un registro durante cualquier período de tiempo...Si desea que sus datos se escriban de manera predecible en la memoria real y no solo que el compilador los almacene en caché en un registro a su discreción, deberá marcarlos como volátiles.Alternativamente, si solo accede a los datos compartidos después de haber dejado una función que los modifica, podría estar bien.Pero sugeriría no confiar en la suerte para asegurarse de que los valores se vuelvan a escribir desde los registros a la memoria.

Especialmente en máquinas ricas en registros (es decir, no x86), las variables pueden vivir durante períodos bastante largos en los registros, y un buen compilador puede almacenar en caché incluso partes de estructuras o estructuras completas en los registros.Por lo tanto, debe usar volátil, pero para mejorar el rendimiento, también copie los valores a las variables locales para el cálculo y luego realice una reescritura explícita.Esencialmente, usar volatile de manera eficiente significa pensar un poco en el almacenamiento de carga en su código C.

En cualquier caso, definitivamente debe utilizar algún tipo de mecanismo de sincronización proporcionado a nivel del sistema operativo para crear un programa correcto.

Para ver un ejemplo de la debilidad de volátil, vea mi ejemplo del algoritmo de Decker en http://jakob.engbloms.se/archives/65, lo que demuestra bastante bien que volatile no funciona para sincronizar.

Otros consejos

Siempre que utilice bloqueos para controlar el acceso a la variable, no necesita volátil en ella.De hecho, si pone volátil en alguna variable, probablemente ya esté equivocado.

https://software.intel.com/en-us/blogs/2007/11/30/volatile-almost-useless-for-multi-threaded-programming/

La respuesta es absoluta e inequívocamente NO.No es necesario utilizar 'volatile' además de las primitivas de sincronización adecuadas.Todo lo que hay que hacer lo hacen estos primitivos.

El uso de "volátil" no es necesario ni suficiente.No es necesario porque las primitivas de sincronización adecuadas son suficientes.No es suficiente porque sólo desactiva algunas optimizaciones, no todas las que podrían molestarte.Por ejemplo, no garantiza ni la atomicidad ni la visibilidad en otra CPU.

Pero a menos que use volátil, el compilador es libre de almacenar en caché los datos compartidos en un registro durante cualquier período de tiempo...Si desea que sus datos se escriban de manera predecible en la memoria real y no solo que el compilador los almacene en caché en un registro a su discreción, deberá marcarlos como volátiles.Alternativamente, si solo accede a los datos compartidos después de haber dejado una función que los modifica, podría estar bien.Pero sugeriría no confiar en la suerte para asegurarse de que los valores se vuelvan a escribir desde los registros a la memoria.

Correcto, pero incluso si usa volátil, la CPU puede almacenar en caché los datos compartidos en un búfer de publicación de escritura durante cualquier período de tiempo.El conjunto de optimizaciones que pueden afectarte no es precisamente el mismo que el conjunto de optimizaciones que "volatile" desactiva.Entonces, si usas 'volatile', son confiando en la suerte ciega.

Por otro lado, si utiliza primitivas de sincronización con semántica multiproceso definida, tiene la garantía de que todo funcionará.Como ventaja, no sufres el enorme impacto en el rendimiento de lo "volátil".Entonces, ¿por qué no hacer las cosas de esa manera?

Existe una noción generalizada de que la palabra clave volátil es buena para la programación multiproceso.

Hans Böhm Señala que sólo hay tres usos portátiles para los volátiles:

  • volátil se puede utilizar para marcar variables locales en el mismo ámbito que un setjmp cuyo valor debe conservarse en un longjmp.No está claro qué fracción de tales usos se ralentizaría, ya que las restricciones de atomicidad y ordenamiento no tienen efecto si no hay forma de compartir la variable local en cuestión.(Ni siquiera está claro qué fracción de dichos usos se ralentizaría si se requiriera que todas las variables se conservaran en un longjmp, pero ese es un asunto aparte y no se considera aquí).
  • volátil se puede utilizar cuando las variables pueden ser "modificadas externamente", pero la modificación en realidad es activada sincrónicamente por el hilo mismo, p.e.porque la memoria subyacente está asignada en múltiples ubicaciones.
  • A volátil sigatomic_t puede usarse para comunicarse con un manejador de señales en el mismo hilo, de manera restringida.Se podría considerar debilitar los requisitos para el caso sigatomic_t, pero eso parece bastante contrario a la intuición.

Si usted es subprocesos múltiples En aras de la velocidad, ralentizar el código definitivamente no es lo que desea.Para la programación multiproceso, hay dos cuestiones clave que a menudo se piensa erróneamente que volátil aborda:

  • atomicidad
  • consistencia de la memoria, es decir.el orden de las operaciones de un hilo vistas por otro hilo.

Tratemos primero con (1).Volatile no garantiza lecturas o escrituras atómicas.Por ejemplo, una lectura o escritura volátil de una estructura de 129 bits no será atómica en la mayoría del hardware moderno.Una lectura o escritura volátil de un int de 32 bits es atómica en la mayoría del hardware moderno, pero volátil no tiene nada que ver con eso.Probablemente sería atómico sin lo volátil.La atomicidad depende del capricho del compilador.No hay nada en los estándares C o C++ que diga que tiene que ser atómico.

Consideremos ahora la cuestión (2).A veces los programadores piensan que volátil significa desactivar la optimización de los accesos volátiles.Esto es en gran medida cierto en la práctica.Pero esos son sólo los accesos volátiles, no los no volátiles.Considere este fragmento:

 volatile int Ready;       

    int Message[100];      

    void foo( int i ) {      

        Message[i/10] = 42;      

        Ready = 1;      

    }

Está intentando hacer algo muy razonable en la programación multiproceso:Escribe un mensaje y luego envíalo a otro hilo.El otro hilo esperará hasta que Listo sea distinto de cero y luego leerá el Mensaje.Intente compilar esto con "gcc -O2 -S" usando gcc 4.0 o icc.Ambos almacenarán primero en Listo, por lo que se puede superponer con el cálculo de i/10.El reordenamiento no es un error del compilador.Es un optimizador agresivo que hace su trabajo.

Se podría pensar que la solución es marcar todas las referencias de memoria como volátiles.Eso es simplemente una tontería.Como dicen las citas anteriores, simplemente ralentizará su código.Peor aún, es posible que no solucione el problema.Incluso si el compilador no reordena las referencias, el hardware sí podría hacerlo.En este ejemplo, el hardware x86 no lo reordenará.Tampoco lo hará un procesador Itanium(TM), porque los compiladores de Itanium insertan barreras de memoria para los almacenes volátiles.Esa es una extensión inteligente de Itanium.Pero chips como Power(TM) se reordenarán.Lo que realmente necesitas para realizar el pedido es vallas de memoria, también llamado barreras de la memoria.Una barrera de memoria evita el reordenamiento de las operaciones de memoria a través de la barrera o, en algunos casos, evita el reordenamiento en una dirección. Volátil no tiene nada que ver con las barreras de memoria.

Entonces, ¿cuál es la solución para la programación multiproceso?Utilice una biblioteca o extensión de lenguaje que implemente la semántica atómica y de valla.Cuando se utiliza según lo previsto, las operaciones en la biblioteca insertarán las barreras correctas.Algunos ejemplos:

  • Hilos POSIX
  • Hilos de Windows(TM)
  • OpenMP
  • TBB

Residencia en artículo de Arch Robinson (Intel)

En mi experiencia, no;solo tiene que excluirse correctamente cuando escribe esos valores, o estructurar su programa de manera que los subprocesos se detengan antes de que necesiten acceder a datos que dependen de las acciones de otro subproceso.Mi proyecto, x264, utiliza este método;Los subprocesos comparten una enorme cantidad de datos, pero la gran mayoría de ellos no necesitan mutex porque son de solo lectura o un subproceso esperará a que los datos estén disponibles y finalizados antes de necesitar acceder a ellos.

Ahora bien, si tiene muchos subprocesos que están muy intercalados en sus operaciones (dependen de la producción de los demás en un nivel muy fino), esto puede ser mucho más difícil; de hecho, en tal caso, yo Considere revisar el modelo de subprocesos para ver si es posible hacerlo de manera más limpia con más separación entre subprocesos.

NO.

Volatile solo se requiere cuando se lee una ubicación de memoria que puede cambiar independientemente de los comandos de lectura/escritura de la CPU.En la situación de subprocesos, la CPU tiene control total de las lecturas/escrituras en la memoria para cada subproceso, por lo tanto, el compilador puede asumir que la memoria es coherente y optimiza las instrucciones de la CPU para reducir el acceso innecesario a la memoria.

El uso principal para volatile es para acceder a E/S asignadas en memoria.En este caso, el dispositivo subyacente puede cambiar el valor de una ubicación de memoria independientemente de la CPU.Si no usas volatile Bajo esta condición, la CPU puede usar un valor de memoria previamente almacenado en caché, en lugar de leer el valor recién actualizado.

Volatile solo sería útil si no necesita ningún retraso entre el momento en que un hilo escribe algo y otro hilo lo lee.Sin embargo, sin algún tipo de cerradura, no tienes idea de cuando el otro hilo escribió los datos, solo que es el valor más reciente posible.

Para valores simples (int y float en sus distintos tamaños), un mutex puede ser excesivo si no necesita un punto de sincronización explícito.Si no utiliza un mutex o bloqueo de algún tipo, debe declarar la variable volátil.Si usa un mutex, ya está todo listo.

Para tipos complicados, debes usar un mutex.Las operaciones en ellos no son atómicas, por lo que se puede leer una versión medio modificada sin mutex.

Volátil significa que tenemos que ir a la memoria para obtener o establecer este valor.Si no configura volátil, el código compilado podría almacenar los datos en un registro durante mucho tiempo.

Lo que esto significa es que debes marcar las variables que compartes entre subprocesos como volátiles para que no tengas situaciones en las que un subproceso comience a modificar el valor pero no escriba su resultado antes de que aparezca un segundo subproceso e intente leer el valor. .

Volatile es una sugerencia del compilador que deshabilita ciertas optimizaciones.El ensamblado de salida del compilador podría haber sido seguro sin él, pero siempre debes usarlo para valores compartidos.

Esto es especialmente importante si NO está utilizando los costosos objetos de sincronización de subprocesos proporcionados por su sistema; por ejemplo, podría tener una estructura de datos donde pueda mantenerla válida con una serie de cambios atómicos.Muchas pilas que no asignan memoria son ejemplos de tales estructuras de datos, porque puede agregar un valor a la pila y luego mover el puntero final o eliminar un valor de la pila después de mover el puntero final.Al implementar una estructura de este tipo, lo volátil se vuelve crucial para garantizar que sus instrucciones atómicas sean realmente atómicas.

La razón subyacente es que la semántica del lenguaje C se basa en una máquina abstracta de un solo subproceso.Y el compilador tiene derecho a transformar el programa siempre que los 'comportamientos observables' del programa en la máquina abstracta permanezcan sin cambios.Puede fusionar accesos a memoria adyacentes o superpuestos, rehacer un acceso a memoria varias veces (al desbordarse el registro, por ejemplo), o simplemente descartar un acceso a memoria, si considera el comportamiento del programa, cuando se ejecuta en un solo hilo, no cambia.Por lo tanto, como puedes sospechar, los comportamientos hacer cambie si se supone que el programa realmente se ejecuta en forma de subprocesos múltiples.

Como señaló Paul Mckenney en un famoso Documento del kernel de Linux:

Se supone que el compilador hará lo que desee con las referencias de memoria que no están protegidas por read_once () y write_once ().Sin ellos, el compilador tiene derecho a hacer todo tipo de transformaciones "creativas", que están cubiertas en la sección de barrera del compilador.

READ_ONCE() y WRITE_ONCE() se definen como conversiones volátiles en variables referenciadas.De este modo:

int y;
int x = READ_ONCE(y);

es equivalente a:

int y;
int x = *(volatile int *)&y;

Por lo tanto, a menos que realice un acceso "volátil", no tiene la seguridad de que el acceso se realice Exactamente una vez, sin importar qué mecanismo de sincronización esté utilizando.Llamar a una función externa (pthread_mutex_lock por ejemplo) puede obligar al compilador a realizar accesos a la memoria de las variables globales.Pero esto sucede sólo cuando el compilador no logra determinar si la función externa cambia estas variables globales o no.Los compiladores modernos que emplean sofisticados análisis entre procedimientos y optimización del tiempo de enlace hacen que este truco sea simplemente inútil.

En resumen, debe marcar las variables compartidas por varios subprocesos como volátiles o acceder a ellas mediante conversiones volátiles.


Como también ha señalado Paul McKenney:

¡He visto el brillo en sus ojos cuando hablan de técnicas de optimización que usted no quisiera que sus hijos conocieran!


Pero mira lo que pasa con C11/C++11.

No lo entiendo.¿Cómo obligan las primitivas de sincronización al compilador a recargar el valor de una variable?¿Por qué no utilizaría simplemente la última copia que ya tiene?

Volátil significa que la variable se actualiza fuera del alcance del código y, por lo tanto, el compilador no puede asumir que conoce su valor actual.Incluso las barreras de memoria son inútiles, ya que el compilador, que no tiene en cuenta las barreras de memoria (¿verdad?), aún podría usar un valor almacenado en caché.

Obviamente, algunas personas asumen que el compilador trata las llamadas de sincronización como barreras de memoria."Casey" supone que hay exactamente una CPU.

Si las primitivas de sincronización son funciones externas y los símbolos en cuestión son visibles fuera de la unidad de compilación (nombres globales, puntero exportado, función exportada que puede modificarlos), entonces el compilador los tratará, o cualquier otra llamada a función externa, como una valla de memoria con respecto a todos los objetos visibles externamente.

De lo contrario, estás solo.Y volatile puede ser la mejor herramienta disponible para hacer que el compilador produzca código correcto y rápido.Sin embargo, generalmente no será portátil cuando necesite volátil y lo que realmente hace por usted depende mucho del sistema y del compilador.

No.

Primero, volatile no es necesario.Existen muchas otras operaciones que proporcionan una semántica multiproceso garantizada que no utiliza volatile.Estos incluyen operaciones atómicas, mutex, etc.

Segundo, volatile No es suficiente.El estándar C no proporciona ninguna garantía sobre el comportamiento multiproceso para las variables declaradas. volatile.

Por lo que al no ser necesario ni suficiente, no tiene mucho sentido utilizarlo.

Una excepción serían plataformas particulares (como Visual Studio) donde tiene semántica multiproceso documentada.

Las variables que se comparten entre subprocesos deben declararse "volátiles".Esto le dice al compilador que cuando un hilo escribe en tales variables, la escritura debe ser a la memoria (en lugar de un registro).

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top