pthread_create - pthreads php
Usando C/Pthreads: ¿las variables compartidas deben ser volátiles? (13)
En mi experiencia, no; solo tiene que mutex correctamente cuando escribe en esos valores, o estructurar su programa 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.
En el lenguaje de programació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 compilador o ninguno de los dos?
Editar para agregar: gracias por las excelentes respuestas. Pero, ¿y si no estás usando bloqueos? ¿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 ...
Volátil significa que tenemos que ir a la memoria para obtener o establecer este valor. Si no establece la volatilidad, el código compilado 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 compilador que desactiva ciertas optimizaciones. El ensamblaje de salida del compilador 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 pilas que no asignan memoria son ejemplos de tales estructuras de datos, porque puede agregar un valor a la pila, luego mover el puntero final o eliminar un valor de la pila 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.
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 bloqueo, 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 bloqueo 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.
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 compilador 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 compilador 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 modificándolo, puede que esté 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 compilador 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 programa correcto.
Para ver un ejemplo de la debilidad de la volatilidad, consulte el ejemplo del algoritmo de Decker en http://jakob.engbloms.se/archives/65 , que demuestra bastante bien que la volatilidad no funciona para sincronizarse.
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 use volátil, el compilador puede almacenar en caché los datos compartidos en un registro por un período de tiempo largo ... si desea que sus datos se escriban para que se escriban predeciblemente en la memoria real y no solo en un registro en caché el compilador a su discreción, tendrá que marcarlo como volátil. Alternativamente, si solo accede a los datos compartidos después de que haya dejado una función modificándolo, puede que esté bien. Pero le sugiero que no confíe en la suerte ciega para asegurarse 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?
No lo entiendo ¿Cómo las primitivas de sincronización obligan al compilador 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 alcance del código y, por lo tanto, el compilador no puede asumir que conoce el valor actual de la misma. 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 en caché.
Algunas personas obviamente están asumiendo que el compilador 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 compilación (nombres globales, puntero exportado, función exportada que puede modificarlos), entonces el compilador 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 compilador 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 mucho del sistema y del compilador.
Las variables que se comparten entre hilos deben declararse ''volátiles''. Esto le dice al compilador que cuando un hilo escribe en tales variables, la escritura debe ser en la memoria (a diferencia de un registro).
Existe una noción generalizada de que la palabra clave volátil es buena para la programació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 programació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 no tiene nada que ver con la volatilidad . Es probable que sea atómico sin el volátil. La atomicidad está al capricho del compilador. 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 programadores 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 programació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 compilar 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 compilador. 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 compilador 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 compiladores 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 programació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)
Siempre que use bloqueos para controlar el acceso a la variable, no necesita volátiles. De hecho, si está poniendo volátil en cualquier variable, probablemente ya esté equivocado.
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.
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 compilador 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 estas condiciones, la CPU puede usar un valor de memoria almacenado previamente en caché, en lugar de leer el valor recién actualizado.
La razón subyacente es que la semántica del lenguaje C se basa en una máquina abstracta de subproceso único . Y el compilador tiene derecho a transformar el programa siempre que los "comportamientos observables" del programa 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 programa, cuando se ejecuta en un solo hilo , no cambia. Por lo tanto, como puede sospechar, los comportamientos cambian si se supone que el programa se está ejecutando en forma de subprocesos múltiples.
Como señaló Paul Mckenney en un famoso documento del kernel de Linux :
No debe suponerse que el compilador hará lo que usted desee con las referencias de memoria que no están protegidas por READ_ONCE () y WRITE_ONCE (). Sin ellos, el compilador 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 compilador a acceder a las variables globales. Pero esto ocurre solo cuando el compilador no puede determinar si la función externa cambia estas variables globales o no. Los compiladores modernos que emplean sofisticados 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 .