¿Es volátil bool para el control de hilo considerado incorrecto?

Como resultado de mi respuesta a esta pregunta , comencé a leer acerca de la palabra clave volatile y cuál es el consenso al respecto. Veo que hay mucha información al respecto, algunas antiguas que ahora parecen incorrectas y muchas otras que dicen que casi no tiene cabida en la progtwigción de subprocesos múltiples. Por lo tanto, me gustaría aclarar un uso específico (no pude encontrar una respuesta exacta aquí en SO).

También quiero señalar que entiendo los requisitos para escribir código multihilo en general y por qué volatile no está resolviendo cosas. Aún así, veo el código que usa volatile para el control de subprocesos en las bases de código en las que trabajo. Además, este es el único caso en que utilizo la palabra clave volatile ya que todos los demás recursos compartidos están sincronizados correctamente.

Digamos que tenemos una clase como:

 class SomeWorker { public: SomeWorker() : isRunning_(false) {} void start() { isRunning_ = true; /* spawns thread and calls run */ } void stop() { isRunning_ = false; } private: void run() { while (isRunning_) { // do something } } volatile bool isRunning_; }; 

Para simplificar, algunas cosas quedan fuera, pero lo esencial es que se crea un objeto que hace algo en un hilo recién generado comprobando un booleano ( volatile ) para saber si debería detenerse. Este valor booleano se establece a partir de otro subproceso cada vez que quiere que el trabajador se detenga.

Mi comprensión ha sido que la razón para usar volatile en este caso específico es simplemente evitar cualquier optimización que lo guarde en un registro para el ciclo. Por lo tanto, lo que resulta en un bucle infinito. No es necesario sincronizar correctamente las cosas, porque el hilo de trabajo eventualmente obtendrá el nuevo valor?

Me gustaría entender si esto se considera completamente incorrecto y si el enfoque correcto es usar una variable sincronizada. ¿Hay alguna diferencia entre comstackdor / architecture / núcleos? Tal vez es solo un enfoque descuidado que vale la pena evitar?

Me gustaría que alguien aclare esto. ¡Gracias!

EDITAR

Me interesaría ver (en código) cómo eliges resolver esto.

volatile puede usarse para tales propósitos. Sin embargo, esta es una extensión del estándar C ++ de Microsoft :

Específico de Microsoft

Los objetos declarados como volátiles son (…)

  • Una escritura en un objeto volátil (escritura volátil) tiene semántica de lanzamiento; (…)
  • Una lectura de un objeto volátil (lectura volátil) tiene Semántica de adquisición; (…)

Esto permite que los objetos volátiles se usen para lockings y liberaciones de memoria en aplicaciones multiproceso. (emph agregado)

Es decir, que entiendo, cuando se utiliza el comstackdor de Visual C ++, un volatile bool es para la mayoría de los propósitos prácticos un atomic .

Se debe tener en cuenta que las versiones VS más recientes agregan un conmutador / volátil que controla este comportamiento, por lo que esto solo se mantiene si /volatile:ms está activo.

No necesita una variable sincronizada , sino una variable atómica . Afortunadamente, puedes usar std::atomic .

La cuestión clave es que si más de un subproceso accede simultáneamente a la misma memoria, a menos que el acceso sea atómico , su progtwig completo dejará de estar en un estado bien definido. Quizás tengas suerte con un bool, que posiblemente se actualice atómicamente en cualquier caso, pero la única forma de estar ofensivamente seguro de que lo estás haciendo bien es usar variables atómicas.

“Ver las bases de código en las que trabajas” probablemente no sea una muy buena medida cuando se trata de aprender progtwigción simultánea. La progtwigción simultánea es tremendamente difícil y muy pocas personas la entienden completamente, y estoy dispuesto a apostar a que la gran mayoría del código homebrew (es decir, no utilizar bibliotecas concurrentes dedicadas) es incorrecto de alguna manera. El problema es que esos errores pueden ser extremadamente difíciles de observar o reproducir, por lo que nunca se sabe.

Editar: No está diciendo en su pregunta cómo se actualiza el bool, por lo que asumo lo peor. Si envuelve toda su operación de actualización en un locking global, por ejemplo, entonces, por supuesto, no hay acceso a la memoria concurrente.

Hay tres problemas principales que enfrenta al realizar subprocesos múltiples:

1) Sincronización y seguridad de hilos. Las variables que se comparten entre varios subprocesos deben estar protegidas de ser escritas por varios subprocesos al mismo tiempo e impedirse su lectura durante las escrituras no atómicas. La sincronización de objetos solo se puede realizar a través de un objeto semáforo / mutex especial que se garantiza que es atómico en sí mismo. La palabra clave volátil no ayuda.

2) tubería de instrucciones. Una CPU puede cambiar el orden en que se ejecutan algunas instrucciones para que el código se ejecute más rápido. En un entorno de múltiples CPU donde se ejecuta un subproceso por CPU, las CPU canalizan las instrucciones sin saber que otra CPU en el sistema está haciendo lo mismo. La protección contra la tubería de instrucción se llama barreras de memoria. Todo se explica bien en Wikipedia . Las barreras de memoria pueden implementarse a través de objetos de barrera de memoria dedicados o a través del objeto semáforo / mutex en el sistema. Un comstackdor podría elegir invocar una barrera de memoria en el código cuando se usa la palabra clave volátil, pero eso sería una excepción bastante especial y no la norma. Nunca asumiría que la palabra clave volátil hizo esto sin tenerlo verificado en el manual del comstackdor.

3) desconocimiento del comstackdor de las funciones de callback. Al igual que para las interrupciones de hardware, algunos comstackdores pueden no saber que se ha ejecutado una función de callback y actualizar un valor en el medio de la ejecución del código. Puedes tener un código como este:

 // main x=true; while(something) { if(x==true) { do_something(); } else { do_seomthing_else(); /* The code may never go here: the compiler doesn't realize that x was changed by the callback. Or worse, the compiler's optimizer could decide to entirely remove this section from the program, as it thinks that x could never be false when the program comes here. */ } } // thread callback function: void thread (void) { x=false; } 

Tenga en cuenta que este problema solo aparece en algunos comstackdores, según la configuración de su optimizador. Este problema particular se resuelve con la palabra clave volátil.


Así que la respuesta a la pregunta es: en un progtwig de subprocesos múltiples, la palabra clave volátil no ayuda con la sincronización / seguridad de subprocesos, probablemente no actúe como una barrera de memoria, pero podría evitar suposiciones peligrosas por el optimizador del comstackdor.

El uso de volatile es suficiente solo en núcleos individuales, donde todos los subprocesos usan el mismo caché. En multinúcleos, si se stop() en un núcleo y run() está ejecutando en otro, la memoria caché de la CPU puede tardar un tiempo en sincronizarse, lo que significa que dos núcleos podrían ver dos vistas diferentes de isRunning_ . Esto significa que run() se ejecutará durante un tiempo después de que se haya detenido.

Si usa mecanismos de sincronización, se asegurarán de que todos los cachés obtengan los mismos valores, a costa de retrasar el progtwig por un tiempo. Si el rendimiento o la corrección es más importante para usted depende de sus necesidades reales.

Esto funcionará para su caso, pero para proteger una sección crítica este enfoque es incorrecto. Si fuera correcto, se podría usar un bool volátil en casi todos los casos en que se utiliza un mutex. El motivo es que una variable volátil no garantiza el cumplimiento de ninguna barrera de memoria ni ningún mecanismo de coherencia de caché. Por el contrario, un mutex hace. En otras palabras, una vez que se bloquea un mutex, se difunde una invalidación de caché a todos los núcleos para mantener la coherencia entre todos los núcleos. Con la volatilidad, este no es el caso. Sin embargo, Andrei Alexandrescu propuso un enfoque muy interesante para usar la volatilidad para imponer la sincronización en un objeto compartido. Y como verán, lo hace con un mutex; volátil solo se usa para evitar el acceso a la interfaz del objeto sin sincronización.