Usando C / Pthreads: ¿las variables compartidas deben ser volátiles?

En el lenguaje de progtwigción C y Pthreads como la biblioteca de threading; ¿las variables / estructuras que se comparten entre subprocesos deben declararse como volátiles? Asumiendo que podrían estar protegidos por un candado o no (barreras tal vez).

¿El estándar pthread POSIX tiene algo que decir sobre esto, es dependiente del comstackdor o ninguno de los dos?

Editar para agregar: gracias por las excelentes respuestas. Pero, ¿y si no estás usando lockings? ¿y si usas barreras por ejemplo? O código que usa primitivas como compare-and-swap para modificar directa y atómicamente una variable compartida …

Creo que una propiedad muy importante de volátil es que hace que la variable se escriba en la memoria cuando se modifique y se vuelva a leer de la memoria cada vez que se acceda. Las otras respuestas aquí combinan la volatilidad y la sincronización, y es claro a partir de algunas otras respuestas que esta volátil que NO es una primitiva de sincronización (crédito donde se debe crédito).

Pero a menos que utilice volátil, el comstackdor puede almacenar en caché los datos compartidos en un registro por un período de tiempo … si desea que sus datos se escriban para que se escriban predeciblemente en la memoria real y no simplemente en un registro en el registro comstackdor a su discreción, deberá marcarlo como volátil. Alternativamente, si solo accede a los datos compartidos después de que haya dejado una función que lo modifique, puede estar bien. Pero sugeriría que no confiara en la suerte ciega para asegurarse de que los valores se escriben 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 comstackdor puede almacenar en caché incluso partes de estructuras o estructuras enteras en registros. Por lo tanto, debe usar la palabra volátil, pero para el rendimiento, también copie valores en variables locales para el cálculo y luego realice una escritura regresiva explícita. Esencialmente, usar volátiles de manera eficiente significa hacer un poco de pensamiento de la tienda de carga en su código C.

En cualquier caso, positivamente tiene que usar algún tipo de mecanismo de sincronización proporcionado por el sistema operativo para crear un progtwig correcto.

Para ver un ejemplo de la debilidad de la volatilidad, vea el ejemplo del algoritmo de Decker en http://jakob.engbloms.se/archives/65 , que demuestra bastante bien que la volatilidad no funciona para sincronizarse.

Siempre que use lockings para controlar el acceso a la variable, no necesita volátiles. De hecho, si está volviendo volátil sobre cualquier 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 absolutamente, inequívocamente, NO. No necesita usar ‘volátil’ además de las primitivas de sincronización adecuadas. Todo lo que se necesita hacer es hecho por 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 solo desactiva algunas optimizaciones, no todas las que podrían morderte. Por ejemplo, no garantiza ni atomicidad ni visibilidad en otra CPU.

Pero a menos que utilice volátil, el comstackdor puede almacenar en caché los datos compartidos en un registro por un período de tiempo … si desea que sus datos se escriban para que se escriban predeciblemente en la memoria real y no simplemente en un registro en el registro comstackdor a su discreción, deberá marcarlo como volátil. Alternativamente, si solo accede a los datos compartidos después de que haya dejado una función que lo modifique, puede estar bien. Pero sugeriría que no confiara en la suerte ciega para asegurarse de que los valores se escriben 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 morderlo no es exactamente el mismo que el conjunto de optimizaciones que ‘volátil’ desactiva. Entonces, si usas ‘volátil’, confías en la suerte ciega.

Por otro lado, si utiliza primitivas de sincronización con semántica de subprocesos múltiples definida, se le garantiza que las cosas funcionarán. Como ventaja, no tomas el gran golpe de ‘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 progtwigción de subprocesos múltiples.

Hans Boehm señala que solo hay tres usos portátiles para la volatilidad:

  • volátil puede usarse para marcar variables locales en el mismo ámbito que un setjmp cuyo valor debe conservarse en longjmp. No está claro qué fracción de dichos usos se ralentizaría, ya que la atomicidad y las restricciones de ordenación no tienen efecto si no hay forma de compartir la variable local en cuestión. (Aún no está claro qué fracción de dichos usos se ralentizaría al requerir que todas las variables se conserven a través de un longjmp, pero ese es un asunto separado y no se considera aquí).
  • volátil puede usarse cuando las variables pueden ser “modificadas externamente”, pero la modificación de hecho se desencadena de forma síncrona por el propio subproceso, por ejemplo, porque la memoria subyacente se mapea en múltiples ubicaciones.
  • Un sigatomic_t volátil puede usarse para comunicarse con un manejador de señal en el mismo hilo, de una manera restringida. Uno podría considerar debilitar los requisitos para el caso sigatomic_t, pero eso parece bastante contrario a la intuición.

Si está realizando varios subprocesos por el bien de la velocidad, la desaceleración del código definitivamente no es lo que desea. Para la progtwigción de subprocesos múltiples, a menudo se piensa erróneamente que hay dos problemas clave que a menudo son volátiles:

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

Vamos a tratar con (1) primero. Volátil no garantiza lecturas o escrituras atómicas. Por ejemplo, una lectura o escritura volátil de una estructura de 129 bits no va a 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 . Es probable que sea atómico sin el volátil. La atomicidad está al capricho del comstackdor. No hay nada en los estándares C o C ++ que diga que tiene que ser atómico.

Ahora considera el problema (2). A veces los progtwigdores piensan en volátiles como la desactivación de la optimización de accesos volátiles. Eso es en gran parte cierto en la práctica. Pero eso son solo los accesos volátiles, no los no volátiles. Considera este fragmento:

  volatile int Ready; int Message[100]; void foo( int i ) { Message[i/10] = 42; Ready = 1; } 

Está tratando de hacer algo muy razonable en la progtwigción de subprocesos múltiples: escribir un mensaje y luego enviarlo a otro hilo. El otro subproceso esperará hasta que Ready sea distinto de cero y luego lea Message. Intente comstackr esto con “gcc -O2 -S” usando gcc 4.0 o icc. Ambos harán la tienda en Ready primero, por lo que puede superponerse con el cálculo de i / 10. El reordenamiento no es un error del comstackdor. Es un optimizador agresivo haciendo su trabajo.

Puede pensar que la solución es marcar todas sus referencias de memoria volátiles. Eso es simplemente tonto. Como dicen las citas anteriores, ralentizará tu código. Lo peor es que podría no solucionar el problema. Incluso si el comstackdor no reordena las referencias, el hardware podría. En este ejemplo, el hardware x86 no lo reordenará. Tampoco lo hará un procesador Itanium (TM), porque los comstackdores Itanium insertan vallas de memoria para las tiendas volátiles. Esa es una inteligente extensión de Itanium. Pero chips como Power (TM) se reordenarán. Lo que realmente necesita para ordenar son cercas de memoria , también llamadas barreras de memoria . Una valla de memoria evita el reordenamiento de las operaciones de memoria a través de la valla, o en algunos casos, evita que se vuelva a ordenar en una dirección. Volatile no tiene nada que ver con vallas de memoria.

Entonces, ¿cuál es la solución para la progtwigción de subprocesos múltiples? Use una extensión de biblioteca o lenguaje que implemente la semántica atómica y de valla. Cuando se usa según lo previsto, las operaciones en la biblioteca insertarán las vallas correctas. Algunos ejemplos:

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

Basado en el artículo de Arch Robison (Intel)

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

Ahora bien, si tiene muchos hilos 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, considere volver a visitar el modelo de subprocesamiento para ver si se puede hacer más limpiamente con más separación entre subprocesos.

NO.

Volatile solo es necesaria 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 enhebrado, la CPU tiene el control total de lectura / escritura en la memoria de cada hilo, por lo tanto, el comstackdor puede suponer que la memoria es coherente y optimiza las instrucciones de la CPU para reducir el acceso innecesario a la memoria.

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

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

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

Para tipos complicados, debe usar un mutex. Las operaciones en ellos no son atómicas, por lo que podría leer una versión a medio cambiar sin un mutex.

Volátil significa que tenemos que ir a la memoria para obtener o establecer este valor. Si no establece la volatilidad, el código comstackdo puede almacenar los datos en un registro durante un tiempo prolongado.

Lo que esto significa es que debe marcar las variables que comparte entre subprocesos como volátiles para que no haya situaciones en las que un subproceso comience a modificar el valor pero no escriba su resultado antes de que aparezca un segundo subproceso y trate de leer el valor .

Volátil es una sugerencia del comstackdor que desactiva ciertas optimizaciones. El ensamblaje de salida del comstackdor podría haber estado seguro sin él, pero siempre debe usarlo para valores compartidos.

Esto es especialmente importante si NO está utilizando los costosos objetos de sincronización de hilos proporcionados por su sistema; por ejemplo, podría tener una estructura de datos donde pueda mantener su validez con una serie de cambios atómicos. Muchas de las stacks que no asignan memoria son ejemplos de tales estructuras de datos, porque puede agregar un valor a la stack, luego mover el puntero final o eliminar un valor de la stack después de mover el puntero final. Al implementar dicha estructura, la volatilidad 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 subproceso único . Y el comstackdor tiene derecho a transformar el progtwig siempre que los “comportamientos observables” del progtwig en la máquina abstracta no cambien. Puede combinar accesos de memoria adyacentes o superpuestos, rehacer un acceso de memoria varias veces (al desbordar el registro, por ejemplo), o simplemente descartar un acceso de memoria, si cree que los comportamientos del progtwig, cuando se ejecuta en un solo hilo , no cambia. Por lo tanto, como puede sospechar, los comportamientos cambian si se supone que el progtwig se está ejecutando de manera multitarea.

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

No debe suponerse que el comstackdor hará lo que usted desee con las referencias de memoria que no están protegidas por READ_ONCE () y WRITE_ONCE (). Sin ellos, el comstackdor tiene derecho a realizar todo tipo de transformaciones “creativas”, que se tratan en la sección BARRERA DE COMPILADORES.

READ_ONCE () y WRITE_ONCE () se definen como conversiones volátiles en variables referenciadas. Así:

 int y; int x = READ_ONCE(y); 

es equivalente a:

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

Entonces, a menos que tenga un acceso ‘volátil’, no se le asegura que el acceso ocurre exactamente una vez , sin importar el mecanismo de sincronización que esté usando. Llamar a una función externa (pthread_mutex_lock por ejemplo) puede obligar al comstackdor a acceder a las variables globales. Pero esto ocurre solo cuando el comstackdor no puede determinar si la función externa cambia estas variables globales o no. Los comstackdores modernos que emplean un sofisticado análisis entre procedimientos y la optimización del tiempo de enlace hacen que este truco sea simplemente inútil.

En resumen, debe marcar las variables compartidas por múltiples hilos volátiles o acceder a ellas mediante moldes volátiles.


Como Paul McKenney también ha señalado:

¡He visto el brillo en sus ojos cuando hablan de técnicas de optimización que no querrían que sus hijos supieran!


Pero mira lo que le sucede a C11 / C ++ 11 .

No lo entiendo ¿Cómo las primitivas de sincronización obligan al comstackdor a volver a cargar el valor de una variable? ¿Por qué no solo usaría la última copia que ya tiene?

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

Algunas personas obviamente están asumiendo que el comstackdor trata las llamadas de sincronización como barreras de memoria. “Casey” asume 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 comstackción (nombres globales, puntero exportado, función exportada que puede modificarlos), entonces el comstackdor los tratará -o cualquier otra llamada de función externa- como un valla de memoria con respecto a todos los objetos visibles externamente.

De lo contrario, estás solo. Y volátil puede ser la mejor herramienta disponible para que el comstackdor produzca código correcto y rápido. Sin embargo, generalmente no será portátil, cuando necesita volátiles y lo que realmente hace para usted depende en gran medida del sistema y del comstackdor.

No.

Primero, volatile no es necesario. Existen numerosas otras operaciones que proporcionan semántica multithreaded garantizada que no usan volatile . Estos incluyen operaciones atómicas, mutexes, etc.

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

Entonces, al no ser necesario ni suficiente, no tiene mucho sentido usarlo.

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

Las variables que se comparten entre hilos deben declararse ‘volátiles’. Esto le dice al comstackdor que cuando un hilo escribe en tales variables, la escritura debe ser en la memoria (a diferencia de un registro).