volatilidad volatiles volatil variable valores significado sigma retornos que precio mide mercado los las finanzas financiera economia diccionario concepto bursátil acciones c++ thread-safety volatile

c++ - volatiles - Puede ser volátil en tipos definidos por el usuario para ayudar a escribir código seguro para subprocesos



volatilidad de los retornos (8)

En el artículo, la palabra clave se usa más como una etiqueta required_thread_safety que el uso real previsto de volatile.

Sin haber leído el artículo, ¿por qué no utiliza Andrei dicha etiqueta required_thread_safety ? Abusar de volatile no suena tan buena idea aquí. Creo que esto causa más confusión (como dijiste), en lugar de evitarla.

Dicho esto, a veces puede requerirse volatile en el código de subprocesos múltiples, incluso si no es una condición suficiente , solo para evitar que el compilador optimice las verificaciones que se basan en la actualización asíncrona de un valor.

Sé que se ha dejado bastante claro en un par de preguntas / respuestas antes, que la volatile está relacionada con el estado visible del modelo de memoria c ++ y no con multihilo.

Por otro lado, este article de Alexandrescu utiliza la palabra clave volatile no como una función de tiempo de ejecución sino como una verificación de tiempo de compilación para forzar al compilador a no poder aceptar un código que podría no ser seguro para subprocesos. En el artículo, la palabra clave se usa más como una etiqueta required_thread_safety que el uso real previsto de volatile .

¿Es este (ab) uso de volatile apropiado? ¿Qué posibles errores pueden estar ocultos en el enfoque?

Lo primero que me viene a la mente es la confusión agregada: la volatile no está relacionada con la seguridad del hilo, pero por falta de una herramienta mejor, podría aceptarlo.

Simplificación básica del artículo:

Si declara una variable volatile , solo se pueden llamar métodos miembro volatile , por lo que el compilador bloqueará el código de llamada a otros métodos. Declarar una instancia de std::vector como volatile bloqueará todos los usos de la clase. Al agregar una envoltura con la forma de un puntero de bloqueo que realiza un const_cast para liberar el requisito volatile , se permitirá cualquier acceso a través del puntero de bloqueo.

Robando del artículo:

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&); }; class SyncBuf { public: void Thread1() { LockingPtr<BufT> lpBuf(buffer_, mtx_); BufT::iterator i = lpBuf->begin(); for (; i != lpBuf->end(); ++i) { // ... use *i ... } } void Thread2(); private: typedef vector<char> BufT; volatile BufT buffer_; Mutex mtx_; // controls access to buffer_ };

NOTA

Después de que hayan aparecido las primeras respuestas, creo que debo aclarar, ya que podría no haber utilizado las palabras más adecuadas.

El uso de volatile no se debe a lo que proporciona en tiempo de ejecución sino a lo que significa en tiempo de compilación. Es decir, el mismo truco se podría extraer con la palabra clave const si se utilizara tan raramente en los tipos definidos por el usuario como lo es la volatile . Es decir, hay una palabra clave (que se escribe de forma volátil) que me permite bloquear las llamadas de funciones de los miembros, y Alexandrescu la está utilizando para engañar al compilador para que no compile el código no seguro para subprocesos.

Lo veo como muchos trucos de metaprogramación que están ahí, no por lo que hacen en tiempo de compilación, sino por lo que obliga al compilador a hacer por ti.


C ++ 03 §7.1.5.1p7:

Si se intenta referirse a un objeto definido con un tipo calificado volátil mediante el uso de un lvalue con un tipo calificado no volátil, el comportamiento del programa no está definido.

Debido a que buffer_ en su ejemplo se define como volátil, desecharlo es un comportamiento indefinido. Sin embargo, puede sortear eso con un adaptador que define el objeto como no volátil, pero agrega volatilidad:

template<class T> struct Lock; template<class T, class Mutex> struct Volatile { Volatile() : _data () {} Volatile(T const &data) : _data (data) {} T volatile& operator*() { return _data; } T const volatile& operator*() const { return _data; } T volatile* operator->() { return &**this; } T const volatile* operator->() const { return &**this; } private: T _data; Mutex _mutex; friend class Lock<T>; };

La amistad es necesaria para controlar estrictamente el acceso no volátil a través de un objeto ya bloqueado:

template<class T> struct Lock { Lock(Volatile<T> &data) : _data (data) { _data._mutex.lock(); } ~Lock() { _data._mutex.unlock(); } T& operator*() { return _data._data; } T* operator->() { return &**this; } private: Volatile<T> &_data; };

Ejemplo:

struct Something { void action() volatile; // Does action in a thread-safe way. void action(); // May assume only one thread has access to the object. int n; }; Volatile<Something> data; void example() { data->action(); // Calls volatile action. Lock<Something> locked (data); locked->action(); // Calls non-volatile action. }

Hay dos advertencias. Primero, aún puede acceder a los miembros de datos públicos (Something :: n), pero serán calificados como volátiles; Esto probablemente fallará en varios puntos. Y segundo, Algo no sabe si realmente se ha definido como volátil y eliminar ese volátil (de "esto" o de los miembros) en los métodos aún será UB si se ha definido de esa manera:

Something volatile v; v.action(); // Compiles, but is UB if action casts away volatile internally.

El objetivo principal se logra: los objetos no tienen que estar conscientes de que se usan de esta manera, y el compilador evitará las llamadas a métodos no volátiles (que son todos los métodos para la mayoría de los tipos) a menos que pase por un bloqueo explícitamente.


Creo que el problema no es sobre la seguridad de subprocesos proporcionada por volatile . No lo hace y el artículo de Andrei dice que no. Aquí, un mutex se utiliza para lograr eso. El problema es si el uso de palabras clave volatile para proporcionar una comprobación de tipo estática junto con el uso de mutex para el código seguro de subprocesos es abuso de la palabra clave volatile . En mi humilde opinión es bastante inteligente, pero me he topado con desarrolladores que no son fanáticos de la comprobación de tipos estrictos solo por el bien de ellos.

OMI, cuando escribe un código para un entorno de subprocesos múltiples, ya hay suficiente precaución para enfatizar en qué se espera que las personas no ignoren las condiciones de carrera y los puntos muertos.

Una desventaja de este enfoque envuelto es que cada operación en el tipo que se envuelve con LockingPtr debe realizarse a través de una función miembro. Eso aumentará un nivel de direccionamiento indirecto que podría afectar considerablemente la comodidad de los desarrolladores en un equipo.

Pero si eres un purista que cree en el espíritu de C ++, también conocido como comprobación de tipo estricto ; esta es una buena alternativa


Debes mejor no hacer eso. La volatilidad ni siquiera se inventó para proporcionar seguridad a los hilos. Se inventó para acceder correctamente a los registros de hardware asignados en memoria. La palabra clave volátil no tiene efecto sobre la función de ejecución fuera de orden de la CPU. Debe usar las llamadas correctas del sistema operativo o las instrucciones CAS definidas por la CPU, las vallas de memoria, etc.

CAS

Cerca de la memoria


Esto detecta algunos tipos de código no seguro para subprocesos (acceso concurrente), pero pierde otros (puntos muertos debido a la inversión de bloqueo). Ninguna de las dos es especialmente fácil de probar, por lo que es una ganancia parcial modesta. En la práctica, recordar hacer cumplir la restricción de que se accede a un miembro privado en particular solo bajo un bloqueo específico, no ha sido un gran problema para mí.

Dos respuestas a esta pregunta han demostrado que es correcto decir que la confusión es una desventaja significativa: los mantenedores pueden haber estado tan fuertemente condicionados a comprender que la semántica de acceso a la memoria de volatile no tiene nada que ver con la seguridad de subprocesos, que ni siquiera lo harán. lea el resto del código / artículo antes de declararlo incorrecto.

Creo que la otra gran desventaja, descrita por Alexandrescu en el artículo, es que no funciona con tipos que no son de clase. Esto podría ser una restricción difícil de recordar. Si cree que marcar la volatile miembros de sus datos le impide usarlos sin bloquearlos, y luego espera que el compilador le indique cuándo bloquearlos, puede aplicarlos accidentalmente a un int o a un miembro del tipo dependiente de parámetros de la plantilla. El código incorrecto resultante se compilará bien, pero es posible que haya dejado de examinar su código en busca de errores de este tipo . Imagine los errores que se producirían, especialmente en el código de plantilla, si fuera posible asignarlos a un const int . const int , pero los programadores esperaban que el compilador verificara la corrección de const para ellos ...

Creo que el riesgo de que el tipo de miembro de datos tenga realmente alguna función de miembro volatile debe ser anotado y luego descontado, aunque podría morder a alguien algún día.

Me pregunto si hay algo que decir sobre los compiladores que proporcionan modificadores de tipo const-style adicionales a través de atributos. Stroustrup dice : "La recomendación es usar atributos para controlar solo las cosas que no afectan el significado de un programa pero que pueden ayudar a detectar errores". Si pudiera reemplazar todas las menciones de volatile en el código con [[__typemodifier(needslocking)]] entonces creo que sería mejor. Entonces sería imposible utilizar el objeto sin un const_cast , y es de esperar que no escriba un const_cast sin pensar en qué es lo que está descartando.


Mira esto desde una perspectiva diferente. Cuando declara una variable como constante, le está diciendo al compilador que su código no puede cambiar el valor. Pero eso no significa que el valor no cambie. Por ejemplo, si haces esto:

const int cv = 123; int* that = const_cast<int*>(&cv); *that = 42;

... Esto evoca un comportamiento indefinido de acuerdo con el estándar, pero en la práctica algo sucederá. Tal vez el valor será cambiado. Tal vez habrá un sigfault. Tal vez el simulador de vuelo se lance, quién sabe. El punto es que no se sabe, independientemente de la plataforma, qué va a pasar. Así que la aparente promesa de const no se cumple. El valor puede o no ser en realidad const.

Ahora, dado que esto es cierto, ¿es un abuso const del lenguaje el uso const ? Por supuesto no. Sigue siendo una herramienta que proporciona el lenguaje para ayudarlo a escribir un mejor código. Nunca será la herramienta definitiva, definitiva, para garantizar que los valores se mantengan sin cambios (el cerebro del programador es, en última instancia, esa herramienta), pero ¿eso hace que la const no sea útil?

Yo digo que no, usar const como una herramienta para ayudarte a escribir un mejor código no es un abuso del lenguaje. De hecho, iría un paso más allá y diría que es la intención de esa característica.

Ahora, lo mismo es cierto de volátil. Declarar algo como volátil no hará que tu programa sea seguro. Probablemente ni siquiera haga que la variable u objeto sea seguro. Pero el compilador aplicará la semántica de calificación de CV, y un cuidadoso programador puede aprovechar este hecho para ayudarlo a escribir mejor código al ayudar al compilador a identificar los lugares donde podría estar escribiendo un error. Al igual que el compilador lo ayuda cuando intenta hacer esto:

const int cv = 123; cv = 42; // ERROR - compiler complains that the programmer is potentially making a mistake

Olvídate de las vallas de memoria y la atomicidad de los objetos y variables volátiles, tal como te has olvidado de la verdadera constancia de cv. Pero usa las herramientas que el lenguaje te da para escribir mejor código. Una de esas herramientas es volatile .


No sé específicamente si el consejo de Alexandrescu es acertado, pero, a pesar de todo lo que lo respeto como un tipo superinteligente, su tratamiento de la semántica de volatile sugiere que se ha alejado mucho de su área de especialización. Volatile no tiene absolutamente ningún valor en los subprocesos múltiples (vea here para un buen tratamiento del tema), por lo que la afirmación de Alexandrescu de que el volátil es útil para el acceso multiproceso me lleva a preguntarme seriamente cuánta fe puedo depositar en el resto de su artículo.


Basándose en otro código y eliminando por completo la necesidad del especificador volátil, esto no solo funciona, sino que también se propaga correctamente (similar a iterador frente a const_iterator). Desafortunadamente, requiere un poco de código de repetición para los dos tipos de interfaz, pero no tiene que repetir ninguna lógica de los métodos: cada uno todavía está definido una vez, incluso si tiene que "duplicar" las versiones "volátiles" de manera similar a la sobrecarga normal de métodos en const y non-const.

#include <cassert> #include <iostream> struct ExampleMutex { // Purely for the sake of this example. ExampleMutex() : _locked (false) {} bool try_lock() { if (_locked) return false; _locked = true; return true; } void lock() { bool acquired = try_lock(); assert(acquired); } void unlock() { assert(_locked); _locked = false; } private: bool _locked; }; // Customization point so these don''t have to be implemented as nested types: template<class T> struct VolatileTraits { typedef typename T::VolatileInterface Interface; typedef typename T::VolatileConstInterface ConstInterface; }; template<class T> class Lock; template<class T> class ConstLock; template<class T, class Mutex=ExampleMutex> struct Volatile { typedef typename VolatileTraits<T>::Interface Interface; typedef typename VolatileTraits<T>::ConstInterface ConstInterface; Volatile() : _data () {} Volatile(T const &data) : _data (data) {} Interface operator*() { return _data; } ConstInterface operator*() const { return _data; } Interface operator->() { return _data; } ConstInterface operator->() const { return _data; } private: T _data; mutable Mutex _mutex; friend class Lock<T>; friend class ConstLock<T>; }; template<class T> struct Lock { Lock(Volatile<T> &data) : _data (data) { _data._mutex.lock(); } ~Lock() { _data._mutex.unlock(); } T& operator*() { return _data._data; } T* operator->() { return &**this; } private: Volatile<T> &_data; }; template<class T> struct ConstLock { ConstLock(Volatile<T> const &data) : _data (data) { _data._mutex.lock(); } ~ConstLock() { _data._mutex.unlock(); } T const& operator*() { return _data._data; } T const* operator->() { return &**this; } private: Volatile<T> const &_data; }; struct Something { class VolatileConstInterface; struct VolatileInterface { // A bit of boilerplate: VolatileInterface(Something &x) : base (&x) {} VolatileInterface const* operator->() const { return this; } void action() const { base->_do("in a thread-safe way"); } private: Something *base; friend class VolatileConstInterface; }; struct VolatileConstInterface { // A bit of boilerplate: VolatileConstInterface(Something const &x) : base (&x) {} VolatileConstInterface(VolatileInterface x) : base (x.base) {} VolatileConstInterface const* operator->() const { return this; } void action() const { base->_do("in a thread-safe way to a const object"); } private: Something const *base; }; void action() { _do("knowing only one thread accesses this object"); } void action() const { _do("knowing only one thread accesses this const object"); } private: void _do(char const *restriction) const { std::cout << "do action " << restriction << ''/n''; } }; int main() { Volatile<Something> x; Volatile<Something> const c; x->action(); c->action(); { Lock<Something> locked (x); locked->action(); } { ConstLock<Something> locked (x); // ConstLock from non-const object locked->action(); } { ConstLock<Something> locked (c); locked->action(); } return 0; }

Compare la clase Algo con lo que el uso de volatilidad de Alexandrescu requeriría:

struct Something { void action() volatile { _do("in a thread-safe way"); } void action() const volatile { _do("in a thread-safe way to a const object"); } void action() { _do("knowing only one thread accesses this object"); } void action() const { _do("knowing only one thread accesses this const object"); } private: void _do(char const *restriction) const volatile { std::cout << "do action " << restriction << ''/n''; } };