tipos sintaxis funciones ejemplos declaracion datos comandos codigos c++ multithreading c++11 volatile

sintaxis - ¿La palabra clave volátil C++ introduce una cerca de memoria?



sintaxis de c++ (13)

¿La palabra clave volátil C ++ introduce una cerca de memoria?

No se requiere un compilador de C ++ que cumpla con la especificación para introducir una cerca de memoria. Su compilador particular podría; Dirija su pregunta a los autores de su compilador.

La función de "volátil" en C ++ no tiene nada que ver con el enhebrado. Recuerde, el propósito de "volátil" es deshabilitar las optimizaciones del compilador para que la lectura de un registro que está cambiando debido a condiciones exógenas no se optimice. ¿Es una dirección de memoria que está siendo escrita por un hilo diferente en una CPU diferente un registro que está cambiando debido a condiciones exógenas? No. De nuevo, si algunos autores de compiladores han elegido tratar las direcciones de memoria en las que se escriben diferentes subprocesos en diferentes CPU como si fueran registros que cambian debido a condiciones exógenas, ese es su negocio; no están obligados a hacerlo. Tampoco se requieren, incluso si introduce una cerca de memoria, para, por ejemplo, garantizar que cada hilo vea un orden consistente de lecturas y escrituras volátiles.

De hecho, volátil es prácticamente inútil para subprocesos en C / C ++. La mejor práctica es evitarlo.

Además: las vallas de memoria son un detalle de implementación de arquitecturas de procesador particulares. En C #, donde volátil está diseñado explícitamente para subprocesos múltiples, la especificación no dice que se introducirán medias cercas, porque el programa podría estar ejecutándose en una arquitectura que no tiene cercas en primer lugar. Más bien, una vez más, la especificación garantiza ciertas garantías (extremadamente débiles) sobre qué optimizaciones evitará el compilador, el tiempo de ejecución y la CPU para establecer ciertas restricciones (extremadamente débiles) sobre cómo se ordenarán algunos efectos secundarios. En la práctica, estas optimizaciones se eliminan mediante el uso de medias cercas, pero ese es un detalle de implementación sujeto a cambios en el futuro.

El hecho de que se preocupe por la semántica de volátil en cualquier idioma en lo que respecta a subprocesos múltiples indica que está pensando en compartir la memoria a través de subprocesos. Considere simplemente no hacer eso. Hace que su programa sea mucho más difícil de entender y mucho más propenso a contener errores sutiles e imposibles de reproducir.

Entiendo que la información volatile informa al compilador que el valor puede cambiarse, pero para lograr esta funcionalidad, ¿necesita el compilador introducir un límite de memoria para que funcione?

Según tengo entendido, la secuencia de operaciones en objetos volátiles no se puede reordenar y se debe preservar. Esto parece implicar que algunas vallas de memoria son necesarias y que realmente no hay forma de evitar esto. ¿Estoy en lo correcto al decir esto?

Hay una discusión interesante en esta pregunta relacionada

Jonathan Wakely escribe :

... El compilador no puede reordenar los accesos a variables volátiles distintas siempre que ocurran en expresiones completas separadas ... justo que volátil es inútil para la seguridad de subprocesos, pero no por las razones que da. No es porque el compilador podría reordenar los accesos a objetos volátiles, sino porque la CPU podría reordenarlos. Las operaciones atómicas y las barreras de memoria evitan que el compilador y la CPU se reordenen

A lo que David Schwartz responde en los comentarios :

... No hay diferencia, desde el punto de vista del estándar C ++, entre el compilador que hace algo y el compilador que emite instrucciones que hacen que el hardware haga algo. Si la CPU puede reordenar los accesos a los volátiles, entonces el estándar no requiere que se mantenga su orden. ...

... El estándar C ++ no hace ninguna distinción sobre lo que hace el reordenamiento. Y no puede argumentar que la CPU puede reordenarlos sin efecto observable, por lo que está bien: el estándar C ++ define su orden como observable. Un compilador cumple con el estándar C ++ en una plataforma si genera código que hace que la plataforma haga lo que el estándar requiere. Si el estándar requiere que los accesos a los volátiles no se reordenen, entonces una plataforma que los reordene no es compatible. ...

Mi punto es que si el estándar C ++ prohíbe que el compilador reordene los accesos a volátiles distintos, según la teoría de que el orden de dichos accesos es parte del comportamiento observable del programa, entonces también requiere que el compilador emita un código que prohíbe que la CPU realice entonces. El estándar no diferencia entre lo que hace el compilador y lo que el código de generación del compilador hace que haga la CPU.

Lo que produce dos preguntas: ¿Alguno de ellos es "correcto"? ¿Qué hacen realmente las implementaciones reales?


La palabra clave volatile esencialmente significa que las lecturas y escrituras de un objeto deben realizarse exactamente como las escribió el programa, y ​​no optimizarse de ninguna manera . El código binario debe seguir el código C o C ++: una carga donde se lee, una tienda donde hay una escritura.

También significa que no debe esperarse que ninguna lectura dé como resultado un valor predecible: el compilador no debe asumir nada sobre una lectura, incluso inmediatamente después de una escritura en el mismo objeto volátil:

volatile int i; i = 1; int j = i; if (j == 1) // not assumed to be true

volatile puede ser la herramienta más importante en la caja de herramientas "C es un lenguaje ensamblador de alto nivel" .

Si la declaración de un objeto volátil es suficiente para garantizar que el comportamiento del código que se ocupa de los cambios asincrónicos depende de la plataforma: diferentes CPU ofrecen diferentes niveles de sincronización garantizada para lecturas y escrituras de memoria normales. Probablemente no debería intentar escribir un código de subprocesamiento múltiple de bajo nivel a menos que sea un experto en el área.

Las primitivas atómicas proporcionan una buena vista de nivel superior de los objetos para subprocesos múltiples que facilita razonar sobre el código. Casi todos los programadores deben usar primitivas atómicas o primitivas que proporcionan exclusiones mutuas como mutexes, bloqueos de lectura-escritura, semáforos u otras primitivas de bloqueo.


Creo que la confusión en torno al volátil y el reordenamiento de instrucciones proviene de las 2 nociones de reordenamiento que hacen las CPU:

  1. Ejecución fuera de orden.
  2. Secuencia de lectura / escritura de memoria como la ven otras CPU (reordenando en el sentido de que cada CPU podría ver una secuencia diferente).

La volátil afecta la forma en que un compilador genera el código suponiendo una ejecución de subproceso único (esto incluye interrupciones). No implica nada sobre las instrucciones de barrera de memoria, sino que impide que un compilador realice ciertos tipos de optimizaciones relacionadas con los accesos a la memoria.
Un ejemplo típico es recuperar un valor de la memoria, en lugar de usar uno almacenado en caché en un registro.

Ejecución fuera de orden

Las CPU pueden ejecutar instrucciones fuera de orden / especulativamente siempre que el resultado final pueda haber sucedido en el código original. Las CPU pueden realizar transformaciones que no están permitidas en los compiladores porque los compiladores solo pueden realizar transformaciones que sean correctas en todas las circunstancias. En contraste, las CPU pueden verificar la validez de estas optimizaciones y retroceder si resultan incorrectas.

Secuencia de lectura / escritura de memoria como la ven otras CPU

El resultado final de una secuencia de instrucciones, el orden efectivo, debe estar de acuerdo con la semántica del código generado por un compilador. Sin embargo, el orden de ejecución real elegido por la CPU puede ser diferente. El orden efectivo como se ve en otras CPU (cada CPU puede tener una vista diferente) puede verse limitado por barreras de memoria.
No estoy seguro de cuánto puede diferir el orden efectivo y real porque no sé en qué medida las barreras de memoria pueden impedir que las CPU realicen una ejecución fuera de orden.

Fuentes:


Depende de qué compilador sea "el compilador". Visual C ++ lo hace, desde 2005. Pero el estándar no lo requiere, por lo que otros compiladores no lo requieren.


El compilador necesita introducir un límite de memoria alrededor de volatile accesos volatile si, y solo si, eso es necesario para hacer los usos para volatile especificados en el trabajo estándar ( setjmp , manejadores de señales, etc.) en esa plataforma en particular.

Tenga en cuenta que algunos compiladores van mucho más allá de lo que exige el estándar C ++ para que los volatile más potentes o útiles en esas plataformas. El código portátil no debe basarse en volatile para hacer algo más allá de lo especificado en el estándar C ++.


El compilador solo inserta una cerca de memoria en la arquitectura Itanium, que yo sepa.

La palabra clave volatile se usa realmente mejor para cambios asincrónicos, por ejemplo, manejadores de señales y registros mapeados en memoria; Por lo general, es la herramienta incorrecta para la programación multiproceso.


En lugar de explicar lo que hace volatile , permíteme explicarte cuándo debes usarlo.

  • Cuando está dentro de un controlador de señal. Debido a que escribir en una variable volatile es prácticamente lo único que el estándar le permite hacer desde un controlador de señal. Desde C ++ 11 puede usar std::atomic para ese propósito, pero solo si atomic no tiene bloqueo.
  • Cuando se trata de setjmp según Intel .
  • Cuando se trata directamente con hardware y desea asegurarse de que el compilador no optimice sus lecturas o escrituras.

Por ejemplo:

volatile int *foo = some_memory_mapped_device; while (*foo) ; // wait until *foo turns false

Sin el especificador volatile , el compilador puede optimizar completamente el ciclo de distancia. El especificador volatile le dice al compilador que no puede asumir que 2 lecturas posteriores devuelven el mismo valor.

Tenga en cuenta que volatile no tiene nada que ver con hilos. El ejemplo anterior no funciona si se escribió un hilo diferente en *foo porque no hay una operación de adquisición involucrada.

En todos los demás casos, el uso de volatile debe considerarse no portátil y no pasar la revisión del código, excepto cuando se trata de compiladores anteriores a C ++ 11 y extensiones de compilador (como msvc''s /volatile:ms switch, que está habilitado por defecto en X86 / I64).


En primer lugar, los estándares C ++ no garantizan las barreras de memoria necesarias para ordenar correctamente las lecturas / escrituras que no son atómicas. Las variables volátiles se recomiendan para usar con MMIO, manejo de señales, etc. En la mayoría de las implementaciones, volátil no es útil para subprocesos múltiples y generalmente no se recomienda.

En cuanto a la implementación de accesos volátiles, esta es la elección del compilador.

Este article , que describe el comportamiento de gcc , muestra que no puede usar un objeto volátil como barrera de memoria para ordenar una secuencia de escrituras en la memoria volátil.

Con respecto al comportamiento de icc , encontré que esta fuente también dice que la volatilidad no garantiza el pedido de accesos a la memoria.

El compilador Microsoft VS2013 tiene un comportamiento diferente. Esta documentation explica cómo lo volátil impone la semántica de Liberación / Adquisición y permite que los objetos volátiles se utilicen en bloqueos / liberaciones en aplicaciones de subprocesos múltiples.

Otro aspecto que debe tenerse en cuenta es que el mismo compilador puede tener un comportamiento diferente. volátil dependiendo de la arquitectura de hardware objetivo . Esta documentation sobre el compilador MSVS 2013 establece claramente los detalles de la compilación con plataformas volátiles para ARM.

Entonces mi respuesta a:

¿La palabra clave volátil C ++ introduce una cerca de memoria?

sería: No garantizado, probablemente no, pero algunos compiladores podrían hacerlo. No debe confiar en el hecho de que sí.


Esto es en gran parte de la memoria, y se basa en pre-C ++ 11, sin hilos. Pero después de haber participado en las discusiones sobre los subprocesos en el comité, puedo decir que el comité nunca tuvo la intención de que la volatile pudiera usarse para la sincronización entre subprocesos. Microsoft lo propuso, pero la propuesta no fue aprobada.

La especificación clave de volatile es que el acceso a un volátil representa un "comportamiento observable", al igual que IO. Del mismo modo, el compilador no puede reordenar o eliminar IO específico, no puede reordenar ni eliminar accesos a un objeto volátil (o más correctamente, accede a través de una expresión lvalue con tipo calificado volátil). La intención original de volátil era, de hecho, soportar IO mapeado en memoria. Sin embargo, el "problema" con esto es que su implementación definida es lo que constituye un "acceso volátil". Y muchos compiladores lo implementan como si la definición fuera "se ha ejecutado una instrucción que lee o escribe en la memoria". Que es una definición legal, aunque inútil, si la implementación lo especifica. (Todavía tengo que encontrar la especificación real para cualquier compilador).

Podría decirse (y es un argumento que acepto), esto viola la intención del estándar, ya que a menos que el hardware reconozca las direcciones como IO mapeado en memoria e inhiba cualquier reordenamiento, etc., ni siquiera puede usar volátil para IO mapeado en memoria, al menos en arquitecturas Sparc o Intel. Sin embargo, ninguno de los comilers que he visto (Sun CC, g ++ y MSC) muestran instrucciones de valla o membranas. (En el momento en que Microsoft propuso extender las reglas para volatile , creo que algunos de sus compiladores implementaron su propuesta y emitieron instrucciones de valla para accesos volátiles. No he verificado lo que hacen los compiladores recientes, pero no me sorprendería si fuera así dependía de alguna opción del compilador. Sin embargo, la versión que verifiqué (creo que era VS6.0) no emitió vallas).


Lo que David pasa por alto es el hecho de que el estándar c ++ especifica el comportamiento de varios hilos que interactúan solo en situaciones específicas y todo lo demás resulta en un comportamiento indefinido. Una condición de carrera que involucra al menos una escritura no está definida si no utiliza variables atómicas.

En consecuencia, el compilador tiene todo el derecho de renunciar a las instrucciones de sincronización, ya que su CPU solo notará la diferencia en un programa que exhibe un comportamiento indefinido debido a la falta de sincronización.


Mientras trabajaba en un video tutorial descargable en línea para gráficos 3D y desarrollo de motores de juegos trabajando con OpenGL moderno. Usamos volatile dentro de una de nuestras clases. El sitio web del tutorial se puede encontrar aquí y el video que trabaja con la volatile palabra clave se encuentra en el Shader Engine video de la serie 98. Estos trabajos no son míos pero están acreditados Marek A. Krzeminski, MASc y este es un extracto de la página de descarga del video.

"Dado que ahora podemos ejecutar nuestros juegos en varios subprocesos, es importante sincronizar los datos entre los subprocesos correctamente. En este video, muestro cómo crear una clase de bloqueo volitil para garantizar que las variables volitile estén sincronizadas correctamente ..."

Y si usted está suscrito a su sitio web y tener acceso a su video de dentro de este video que hace referencia este article en relación con el uso de Volatile la multithreading programación.

Aquí está el artículo del enlace de arriba: article

volátil: el mejor amigo del programador multiproceso

Por Andrei Alexandrescu, 01 de febrero de 2001

La palabra clave volátil se diseñó para evitar optimizaciones del compilador que podrían hacer que el código sea incorrecto en presencia de ciertos eventos asincrónicos.

No quiero estropear tu estado de ánimo, pero esta columna aborda el temido tema de la programación multiproceso. Si, como dice la entrega anterior de Generic, la programación segura para excepciones es difícil, es un juego de niños en comparación con la programación multiproceso.

Los programas que usan múltiples hilos son notoriamente difíciles de escribir, demuestran ser correctos, depurar, mantener y domesticar en general. Los programas multiproceso incorrectos pueden ejecutarse durante años sin problemas técnicos, solo para ejecutarse de forma inesperada debido a que se ha cumplido alguna condición de sincronización crítica.

No hace falta decir que un programador que escribe código multiproceso necesita toda la ayuda que pueda obtener. Esta columna se centra en las condiciones de carrera, una fuente común de problemas en los programas multiproceso, y le proporciona información y herramientas sobre cómo evitarlas y, sorprendentemente, hace que el compilador trabaje duro para ayudarlo con eso.

Solo una pequeña palabra clave

Aunque los estándares C y C ++ son notablemente silenciosos cuando se trata de subprocesos, hacen una pequeña concesión a los subprocesos múltiples, en forma de la palabra clave volátil.

Al igual que su contraparte más conocida, volátil es un modificador de tipo. Está destinado a usarse junto con variables a las que se accede y se modifica en diferentes subprocesos. Básicamente, sin volátiles, escribir programas multiproceso se vuelve imposible o el compilador desperdicia grandes oportunidades de optimización. Una explicación está en orden.

Considere el siguiente código:

class Gadget { public: void Wait() { while (!flag_) { Sleep(1000); // sleeps for 1000 milliseconds } } void Wakeup() { flag_ = true; } ... private: bool flag_; };

El propósito de Gadget :: Wait anterior es verificar la variable flag_ member cada segundo y regresar cuando esa variable ha sido establecida en true por otro hilo. Al menos eso es lo que pretendía su programador, pero, por desgracia, Wait es incorrecto.

Supongamos que el compilador descubre que Sleep (1000) es una llamada a una biblioteca externa que no puede modificar la variable miembro flag_. Luego, el compilador concluye que puede almacenar en caché flag_ en un registro y usar ese registro en lugar de acceder a la memoria interna más lenta. Esta es una excelente optimización para el código de un solo subproceso, pero en este caso, daña la corrección: después de llamar a Wait para algún objeto Gadget, aunque otro hilo llama Wakeup, Wait se repetirá para siempre. Esto se debe a que el cambio de flag_ no se reflejará en el registro que almacena en caché flag_. La optimización es demasiado ... optimista.

El almacenamiento en caché de variables en los registros es una optimización muy valiosa que se aplica la mayor parte del tiempo, por lo que sería una pena desperdiciarla. C y C ++ le dan la oportunidad de deshabilitar explícitamente dicho almacenamiento en caché. Si usa el modificador volátil en una variable, el compilador no almacenará en caché esa variable en los registros; cada acceso alcanzará la ubicación de memoria real de esa variable. Entonces, todo lo que tiene que hacer para que funcione el combo Wait / Wakeup de Gadget es calificar flag_ adecuadamente:

class Gadget { public: ... as above ... private: volatile bool flag_; };

La mayoría de las explicaciones de la justificación y el uso de volátiles se detienen aquí y le aconsejan que califique volátil los tipos primitivos que usa en múltiples hilos. Sin embargo, hay mucho más que puede hacer con volátil, porque es parte del maravilloso sistema de tipos de C ++.

Uso de volátiles con tipos definidos por el usuario

Puede calificar de forma volátil no solo los tipos primitivos, sino también los tipos definidos por el usuario. En ese caso, volátil modifica el tipo de manera similar a const. (También puede aplicar const y volátil al mismo tipo simultáneamente).

A diferencia de const, la volatilidad discrimina entre tipos primitivos y tipos definidos por el usuario. Es decir, a diferencia de las clases, los tipos primitivos aún admiten todas sus operaciones (suma, multiplicación, asignación, etc.) cuando están calificados para volátiles. Por ejemplo, puede asignar un int no volátil a un int volátil, pero no puede asignar un objeto no volátil a un objeto volátil.

Vamos a ilustrar cómo funciona volátil en tipos definidos por el usuario en un ejemplo.

class Gadget { public: void Foo() volatile; void Bar(); ... private: String name_; int state_; }; ... Gadget regularGadget; volatile Gadget volatileGadget;

Si crees que lo volátil no es tan útil con los objetos, prepárate para una sorpresa.

volatileGadget.Foo(); // ok, volatile fun called for // volatile object regularGadget.Foo(); // ok, volatile fun called for // non-volatile object volatileGadget.Bar(); // error! Non-volatile function called for // volatile object!

La conversión de un tipo no calificado a su contraparte volátil es trivial. Sin embargo, al igual que con const, no puede hacer que el viaje regrese de volátil a no calificado. Debes usar un yeso:

Gadget& ref = const_cast<Gadget&>(volatileGadget); ref.Bar(); // ok

Una clase calificada volátil da acceso solo a un subconjunto de su interfaz, un subconjunto que está bajo el control del implementador de la clase. Los usuarios pueden obtener acceso completo a la interfaz de ese tipo solo usando un const_cast. Además, al igual que la constante, la volatilidad se propaga de la clase a sus miembros (por ejemplo, volatileGadget.name_ y volatileGadget.state_ son variables volátiles).

volátil, secciones críticas y condiciones de carrera

El dispositivo de sincronización más simple y más utilizado en programas multiproceso es el mutex. Un mutex expone las primitivas Adquirir y liberar. Una vez que llame a Adquirir en algún hilo, se bloqueará cualquier otro hilo que llame a Adquirir. Más tarde, cuando ese hilo llama Liberación, se liberará precisamente un hilo bloqueado en una llamada Adquirir. En otras palabras, para un mutex dado, solo un subproceso puede obtener tiempo de procesador entre una llamada a Adquirir y una llamada a Liberar. El código de ejecución entre una llamada a Adquirir y una llamada a Liberar se denomina sección crítica. (La terminología de Windows es un poco confusa porque llama al mutex en sí mismo una sección crítica, mientras que "mutex" es en realidad un mutex entre procesos. Hubiera sido bueno si se llamaran hilo mutex y proceso mutex).

Mutexes se utilizan para proteger los datos contra las condiciones de carrera. Por definición, una condición de carrera ocurre cuando el efecto de más subprocesos en los datos depende de cómo se programan los subprocesos. Las condiciones de carrera aparecen cuando dos o más hilos compiten por usar los mismos datos. Debido a que los hilos pueden interrumpirse entre sí en momentos arbitrarios en el tiempo, los datos pueden corromperse o malinterpretarse. En consecuencia, los cambios y, a veces, los accesos a los datos deben protegerse cuidadosamente con secciones críticas. En la programación orientada a objetos, esto generalmente significa que almacena un mutex en una clase como una variable miembro y lo usa cada vez que accede al estado de esa clase.

Los programadores multiproceso experimentados pueden haber bostezado leyendo los dos párrafos anteriores, pero su propósito es proporcionar un entrenamiento intelectual, porque ahora nos conectaremos con la conexión volátil. Hacemos esto trazando un paralelo entre el mundo de los tipos C ++ y el mundo semántico de subprocesos.

  • Fuera de una sección crítica, cualquier hilo puede interrumpir a cualquier otro en cualquier momento; no hay control, por lo tanto, las variables accesibles desde múltiples hilos son volátiles. Esto está en consonancia con la intención original de volátil: evitar que el compilador almacene en caché involuntariamente los valores utilizados por varios subprocesos a la vez.
  • Dentro de una sección crítica definida por un mutex, solo un hilo tiene acceso. En consecuencia, dentro de una sección crítica, el código de ejecución tiene una semántica de subproceso único. La variable controlada ya no es volátil: puede eliminar el calificador volátil.

En resumen, los datos compartidos entre subprocesos son conceptualmente volátiles fuera de una sección crítica y no volátiles dentro de una sección crítica.

Entras en una sección crítica bloqueando un mutex. Elimina el calificador volátil de un tipo aplicando un const_cast. Si logramos unir estas dos operaciones, creamos una conexión entre el sistema de tipos de C ++ y la semántica de subprocesos de una aplicación. Podemos hacer que el compilador verifique las condiciones de carrera por nosotros.

LockingPtr

Necesitamos una herramienta que recopile una adquisición de mutex y un const_cast. Desarrollemos una plantilla de clase LockingPtr que inicialice con un objeto volátil obj y un mutex mtx. Durante su vida útil, un LockingPtr mantiene mtx adquirido. Además, LockingPtr ofrece acceso al obj despojado de volátiles. El acceso se ofrece en forma de puntero inteligente, a través de operator-> y operator *. Const_cast se realiza dentro de LockingPtr. La conversión es semánticamente válida porque LockingPtr mantiene el mutex adquirido durante su vida útil.

Primero, definamos el esqueleto de una clase Mutex con la que funcionará LockingPtr:

class Mutex { public: void Acquire(); void Release(); ... };

Para usar LockingPtr, implemente Mutex utilizando las estructuras de datos nativas y las funciones primitivas de su sistema operativo.

LockingPtr está diseñado con el tipo de variable controlada. Por ejemplo, si desea controlar un widget, utiliza un LockingPtr que inicializa con una variable de tipo Widget volátil.

La definición de LockingPtr es muy simple. LockingPtr implementa un puntero inteligente poco sofisticado. Se centra únicamente en recopilar una const_cast y una sección crítica.

template <typename T> class LockingPtr { public: // Constructors/destructors LockingPtr(volatile T& obj, Mutex& mtx) : pObj_(const_cast<T*>(&obj)), pMtx_(&mtx) { mtx.Lock(); } ~LockingPtr() { pMtx_->Unlock(); } // Pointer behavior T& operator*() { return *pObj_; } T* operator->() { return pObj_; } private: T* pObj_; Mutex* pMtx_; LockingPtr(const LockingPtr&); LockingPtr& operator=(const LockingPtr&); };

A pesar de su simplicidad, LockingPtr es una ayuda muy útil para escribir código multiproceso correcto. Debe definir los objetos que se comparten entre subprocesos como volátiles y nunca usar const_cast con ellos; siempre use objetos automáticos LockingPtr. Vamos a ilustrar esto con un ejemplo.

Supongamos que tiene dos hilos que comparten un objeto vectorial:

class SyncBuf { public: void Thread1(); void Thread2(); private: typedef vector<char> BufT; volatile BufT buffer_; Mutex mtx_; // controls access to buffer_ };

Dentro de una función de subproceso, simplemente usa un LockingPtr para obtener acceso controlado a la variable de miembro buffer_:

void SyncBuf::Thread1() { LockingPtr<BufT> lpBuf(buffer_, mtx_); BufT::iterator i = lpBuf->begin(); for (; i != lpBuf->end(); ++i) { ... use *i ... } }

El código es muy fácil de escribir y comprender: siempre que necesite usar buffer_, debe crear un LockingPtr que apunte a él. Una vez que haga eso, tendrá acceso a toda la interfaz del vector.

Lo bueno es que si comete un error, el compilador lo señalará:

void SyncBuf::Thread2() { // Error! Cannot access ''begin'' for a volatile object BufT::iterator i = buffer_.begin(); // Error! Cannot access ''end'' for a volatile object for ( ; i != lpBuf->end(); ++i ) { ... use *i ... } }

No puede acceder a ninguna función de buffer_ hasta que aplique un const_cast o use LockingPtr. La diferencia es que LockingPtr ofrece una forma ordenada de aplicar const_cast a variables volátiles.

LockingPtr es notablemente expresivo. Si solo necesita llamar a una función, puede crear un objeto LockingPtr temporal sin nombre y usarlo directamente:

unsigned int SyncBuf::Size() { return LockingPtr<BufT>(buffer_, mtx_)->size(); }

Volver a los tipos primitivos

Vimos cuán agradablemente volátil protege los objetos contra el acceso no controlado y cómo LockingPtr proporciona una forma simple y efectiva de escribir código seguro para subprocesos. Volvamos ahora a los tipos primitivos, que son tratados de manera diferente por volátiles.

Consideremos un ejemplo donde múltiples hilos comparten una variable de tipo int.

class Counter { public: ... void Increment() { ++ctr_; } void Decrement() { —ctr_; } private: int ctr_; };

Si se debe llamar a Incremento y Decremento desde diferentes hilos, el fragmento anterior tiene errores. Primero, ctr_ debe ser volátil. Segundo, incluso una operación aparentemente atómica como ++ ctr_ es en realidad una operación de tres etapas. La memoria en sí no tiene capacidades aritméticas. Al incrementar una variable, el procesador:

  • Lee esa variable en un registro
  • Incrementa el valor en el registro
  • Escribe el resultado en la memoria

Esta operación de tres pasos se llama RMW (lectura-modificación-escritura). Durante la parte Modificar de una operación RMW, la mayoría de los procesadores liberan el bus de memoria para dar acceso a la memoria a otros procesadores.

Si en ese momento otro procesador realiza una operación RMW en la misma variable, tenemos una condición de carrera: la segunda escritura sobrescribe el efecto de la primera.

Para evitar eso, puede confiar, nuevamente, en LockingPtr:

class Counter { public: ... void Increment() { ++*LockingPtr<int>(ctr_, mtx_); } void Decrement() { —*LockingPtr<int>(ctr_, mtx_); } private: volatile int ctr_; Mutex mtx_; };

Ahora el código es correcto, pero su calidad es inferior en comparación con el código de SyncBuf. ¿Por qué? Porque con Counter, el compilador no te avisará si accedes por error a ctr_ directamente (sin bloquearlo). El compilador compila ++ ctr_ si ctr_ es volátil, aunque el código generado es simplemente incorrecto. El compilador ya no es tu aliado, y solo tu atención puede ayudarte a evitar las condiciones de carrera.

¿Qué deberías hacer entonces? Simplemente encapsule los datos primitivos que usa en estructuras de nivel superior y use volátiles con esas estructuras. Paradójicamente, es peor usar volátiles directamente con los incorporados, ¡a pesar de que inicialmente era la intención de uso de volátiles!

Funciones de miembros volátiles

Hasta ahora, hemos tenido clases que agregan miembros de datos volátiles; Ahora pensemos en diseñar clases que a su vez serán parte de objetos más grandes y compartidas entre hilos. Aquí es donde las funciones de miembros volátiles pueden ser de gran ayuda.

Al diseñar su clase, califica de forma volátil solo aquellas funciones miembro que son seguras para subprocesos. Debe asumir que el código del exterior llamará a las funciones volátiles de cualquier código en cualquier momento. No olvide: volátil equivale a código multiproceso libre y sin sección crítica; no volátil equivale a un escenario de subproceso único o dentro de una sección crítica.

Por ejemplo, define un widget de clase que implementa una operación en dos variantes: una segura para subprocesos y una rápida y sin protección.

class Widget { public: void Operation() volatile; void Operation(); ... private: Mutex mtx_; };

Observe el uso de sobrecarga. Ahora el usuario de Widget puede invocar Operación usando una sintaxis uniforme, ya sea para objetos volátiles y obtener seguridad de subprocesos, o para objetos normales y obtener velocidad. El usuario debe tener cuidado al definir los objetos Widget compartidos como volátiles.

Al implementar una función de miembro volátil, la primera operación generalmente es bloquear esto con un LockingPtr. Luego, el trabajo se realiza utilizando el hermano no volátil:

void Widget::Operation() volatile { LockingPtr<Widget> lpThis(*this, mtx_); lpThis->Operation(); // invokes the non-volatile function }

Resumen

Al escribir programas multiproceso, puede utilizar volátiles para su ventaja. Debe cumplir con las siguientes reglas:

  • Defina todos los objetos compartidos como volátiles.
  • No utilice volátiles directamente con tipos primitivos.
  • Al definir clases compartidas, use funciones de miembros volátiles para expresar la seguridad de subprocesos.

Si hace esto, y si usa el componente genérico simple LockingPtr, puede escribir código seguro para subprocesos y preocuparse mucho menos por las condiciones de carrera, porque el compilador se preocupará por usted y señalará diligentemente los puntos donde está equivocado.

Un par de proyectos en los que he estado involucrado usan volátiles y LockingPtr con gran efecto. El código es limpio y entendible. Recuerdo un par de puntos muertos, pero prefiero los puntos muertos a las condiciones de carrera porque son mucho más fáciles de depurar. Prácticamente no hubo problemas relacionados con las condiciones de carrera. Pero entonces nunca se sabe.

Agradecimientos

Muchas gracias a James Kanze y Sorin Jianu que ayudaron con ideas perspicaces.

Andrei Alexandrescu es Gerente de Desarrollo en RealNetworks Inc. (www.realnetworks.com), con sede en Seattle, WA, y autor del aclamado libro Modern C ++ Design. Puede ser contactado en www.moderncppdesign.com. Andrei también es uno de los instructores destacados del Seminario C ++ (www.gotw.ca/cpp_seminar).

Este artículo puede estar un poco anticuado, pero brinda una buena idea del uso excelente del uso del modificador volátil con el uso de programación multiproceso para ayudar a mantener los eventos asíncronos mientras el compilador comprueba las condiciones de carrera para nosotros. Es posible que esto no responda directamente a la pregunta original de los OP sobre la creación de una cerca de memoria, pero elijo publicar esto como una respuesta para otros como una excelente referencia hacia un buen uso de volátil cuando se trabaja con aplicaciones multiproceso.


No tiene que hacerlo La volátil no es una sincronización primitiva. Simplemente deshabilita las optimizaciones, es decir, obtiene una secuencia predecible de lecturas y escrituras dentro de un hilo en el mismo orden que prescribe la máquina abstracta. Pero las lecturas y escrituras en diferentes hilos no tienen orden en primer lugar, no tiene sentido hablar de preservar o no preservar su orden. El orden entre los cabezales se puede establecer mediante primitivas de sincronización, obtienes UB sin ellas.

Un poco de explicación sobre las barreras de memoria. Una CPU típica tiene varios niveles de acceso a la memoria. Hay una tubería de memoria, varios niveles de caché, luego RAM, etc.

Las instrucciones membaras enjuagan la tubería. No cambian el orden en que se ejecutan las lecturas y escrituras, solo obliga a ejecutar las pendientes en un momento dado. Es útil para programas multiproceso, pero no mucho más.

Caché (s) normalmente son coherentes automáticamente entre las CPU. Si uno quiere asegurarse de que el caché esté sincronizado con la RAM, se necesita vaciar el caché. Es muy diferente de un membar.


Siempre uso volátiles en las rutinas de servicio de interrupción, por ejemplo, el ISR (a menudo código de ensamblaje) modifica alguna ubicación de memoria y el código de nivel superior que se ejecuta fuera del contexto de interrupción accede a la ubicación de memoria a través de un puntero a volátil.

Hago esto para RAM, así como para IO mapeado en memoria.

Según la discusión aquí, parece que este sigue siendo un uso válido de volátil, pero no tiene nada que ver con múltiples subprocesos o CPU. Si el compilador para un microcontrolador "sabe" que no puede haber otros accesos (por ejemplo, todo está en el chip, no hay caché y solo hay un núcleo), pensaría que no hay una cerca de memoria, el compilador solo necesita evitar ciertas optimizaciones.

A medida que acumulamos más cosas en el "sistema" que ejecuta el código objeto, casi todas las apuestas están apagadas, al menos así es como leo esta discusión. ¿Cómo podría un compilador cubrir todas las bases?