Je sais, quelques questions / réponses ont clairement montré que volatile
est lié à l’état visible du modèle de mémoire c ++ et non au multithreading.
D’autre part, cet article d’Alexandrescu utilise le mot-clé volatile
non pas comme une fonctionnalité d’exécution, mais plutôt comme une vérification de la compilation pour forcer le compilateur à accepter un code qui pourrait ne pas être thread-safe. Dans l’article, le mot clé est utilisé davantage comme une balise required_thread_safety
que pour l’utilisation réelle de volatile
.
Cette utilisation de volatile
appropriée? Quels pièges possibles peuvent être cachés dans l’approche?
La première chose qui me vient à l’esprit est une confusion supplémentaire: la volatile
n’est pas liée à la sécurité du fil, mais, faute d’un meilleur outil, je pourrais l’accepter.
Simplification de base de l’article:
Si vous déclarez une variable volatile
, seules volatile
méthodes membres volatile
peuvent être appelées. Le compilateur bloque le code appelant d’autres méthodes. Déclarer une instance de std::vector
tant que volatile
bloquera toutes les utilisations de la classe. L’ajout d’un wrapper sous la forme d’un pointeur de locking qui effectue un const_cast
pour libérer l’exigence volatile
, tout access via le pointeur de locking sera autorisé.
Voler l’article:
template class LockingPtr { public: // Constructors/destructors LockingPtr(volatile T& obj, Mutex& mtx) : pObj_(const_cast(&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 lpBuf(buffer_, mtx_); BufT::iterator i = lpBuf->begin(); for (; i != lpBuf->end(); ++i) { // ... use *i ... } } void Thread2(); private: typedef vector BufT; volatile BufT buffer_; Mutex mtx_; // controls access to buffer_ };
REMARQUE
Après que les premières réponses soient parues, je pense que je dois clarifier les choses, car je n’ai peut-être pas utilisé les mots les plus appropriés.
L’utilisation de volatile
n’est pas due à ce qu’elle fournit au moment de l’exécution, mais à ce qu’elle signifie au moment de la compilation. C’est-à-dire que le même mot-clé pourrait être utilisé avec le mot-clé const
s’il était aussi rarement utilisé dans les types définis par l’utilisateur que volatile
. C’est-à-dire qu’il existe un mot clé (qui se trouve être orthographié “volatile”) qui me permet de bloquer les appels de fonctions des membres, et Alexandrescu l’utilise pour amener le compilateur à ne pas comstackr du code non sécurisé par thread.
Je vois autant de trucs de métaprogrammation qui ne sont pas là pour ce qu’ils font au moment de la compilation, mais plutôt pour ce qu’il force le compilateur à faire pour vous.
Je pense que le problème ne concerne pas la sécurité des threads fournie par volatile
. Ce n’est pas le cas et l’ article d’Andrei ne dit pas que c’est le cas. Ici, un mutex
est utilisé pour y parvenir. La question est de savoir si l’utilisation du mot clé volatile
pour fournir une vérification de type statique en même temps que l’utilisation de mutex pour un code thread-safe, constitue-t-elle un abus du mot clé volatile
? À mon humble avis, c’est assez intelligent, mais je suis tombé sur des développeurs qui ne sont pas fans de la vérification de type ssortingcte, rien que pour le plaisir de le faire.
IMO, lorsque vous écrivez du code pour un environnement multithread, il y a déjà suffisamment de prudence pour que vous vous attendiez à ce que les gens n’ignorent pas les conditions de concurrence et les blocages.
Un inconvénient de cette approche LockingPtr
est que chaque opération sur le type LockingPtr
aide de LockingPtr
doit être effectuée via une fonction membre. Cela augmentera le niveau d’indirection qui pourrait affecter considérablement le confort des développeurs dans une équipe.
Mais si vous êtes un puriste qui croit en l’esprit du C ++, aussi appelé vérification de type ssortingcte ; c’est une bonne alternative.
Cela intercepte certains types de code non sécurisé par les threads (access simultané), mais il en omet d’autres (blocages dus à l’inversion de locking). Aucun des deux n’est particulièrement facile à tester, c’est donc une victoire partielle modeste. En pratique, me rappeler d’appliquer une contrainte selon laquelle un membre privé particulier n’est accessible que sous un verrou spécifique n’a pas été un gros problème.
Deux réponses à cette question ont montré que vous aviez raison de dire que la confusion est un inconvénient majeur: les responsables de la maintenance ont peut-être été si fortement conditionnés à comprendre que la sémantique d’access à la mémoire de volatile n’a rien à voir avec la sécurité des threads. lisez le rest du code / de l’article avant de le déclarer incorrect.
Je pense que l’autre gros inconvénient, souligné par Alexandrescu dans l’article, est qu’il ne fonctionne pas avec les types sans classe. Cela pourrait être une ressortingction difficile à retenir. Si vous pensez que le marquage de vos membres de données comme étant volatile
vous empêche de les utiliser sans locking, et que le compilateur vous dise quand verrouiller, vous pouvez accidentellement appliquer cela à un int
ou à un membre du type dépendant de template-paramètre. Le code incorrect qui en résulte comstackra bien, mais vous avez peut-être cessé d’examiner votre code pour rechercher des erreurs de ce type . Imaginez les erreurs qui pourraient se produire, en particulier dans le code de modèle, s’il était possible d’affecter un const int
, mais les programmeurs s’attendaient néanmoins à ce que le compilateur vérifie la correction de const pour eux …
Je pense que le risque que le type de membre de données ait réellement volatile
fonctions de membre volatile
doit être noté et ensuite escompté, bien que cela puisse piquer quelqu’un un jour.
Je me demande s’il y a quelque chose à dire pour les compilateurs fournissant des modificateurs de type const supplémentaires via des atsortingbuts. Stroustrup déclare : “La recommandation est d’utiliser des atsortingbuts pour contrôler uniquement les choses qui n’affectent pas la signification d’un programme, mais qui pourraient aider à détecter les erreurs”. Si vous pouviez remplacer toutes les mentions de volatile
dans le code par [[__typemodifier(needslocking)]]
alors je pense que ce serait mieux. Il serait alors impossible d’utiliser l’object sans une const_cast
, et j’espère que vous const_cast
pas une const_cast
sans penser à ce que vous êtes en train de supprimer.
C ++ 03 §7.1.5.1p7:
Si vous tentez de faire référence à un object défini avec un type qualifié volatil via l’utilisation d’une valeur lvalue avec un type qualifié non volatile, le comportement du programme n’est pas défini.
Dans la mesure où tampon_ dans votre exemple est défini comme volatile, son comportement est indéfini. Cependant, vous pouvez contourner cela avec un adaptateur qui définit l’object comme non volatile, mais ajoute de la volatilité:
template struct Lock; template 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; };
Cette amitié est nécessaire pour contrôler ssortingctement les access non volatiles via un object déjà verrouillé:
template struct Lock { Lock(Volatile &data) : _data (data) { _data._mutex.lock(); } ~Lock() { _data._mutex.unlock(); } T& operator*() { return _data._data; } T* operator->() { return &**this; } private: Volatile &_data; };
Exemple:
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 data; void example() { data->action(); // Calls volatile action. Lock locked (data); locked->action(); // Calls non-volatile action. }
Il y a deux mises en garde. Premièrement, vous pouvez toujours accéder aux membres de données publiques (Something :: n), mais ils seront qualifiés de volatiles; cela va probablement échouer à différents moments. Et deuxièmement, Quelque chose ne sait pas s’il a vraiment été défini comme volatile et le fait de rejeter cette volatilité (de “this” ou des membres) dans les méthodes sera toujours UB s’il a été défini ainsi:
Something volatile v; v.action(); // Comstacks, but is UB if action casts away volatile internally.
L’objective principal est atteint: les objects n’ont pas besoin de savoir qu’ils sont utilisés de cette façon et le compilateur empêchera les appels aux méthodes non volatiles (qui sont toutes des méthodes pour la plupart des types), sauf si vous verrouillez explicitement.
S’appuyant sur un autre code et supprimant entièrement le besoin du spécificateur volatile, cela fonctionne non seulement, mais propage correctement const (similaire à iterator vs const_iterator). Malheureusement, cela nécessite un peu de code standard pour les deux types d’interface, mais vous n’avez pas à répéter la logique des méthodes: chacune est toujours définie une fois, même si vous devez “dupliquer” les versions “volatiles” de la même manière. à la surcharge normale des méthodes sur const et non-const.
#include #include 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 struct VolatileTraits { typedef typename T::VolatileInterface Interface; typedef typename T::VolatileConstInterface ConstInterface; }; template class Lock; template class ConstLock; template struct Volatile { typedef typename VolatileTraits::Interface Interface; typedef typename VolatileTraits ::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 ; friend class ConstLock ; }; template struct Lock { Lock(Volatile &data) : _data (data) { _data._mutex.lock(); } ~Lock() { _data._mutex.unlock(); } T& operator*() { return _data._data; } T* operator->() { return &**this; } private: Volatile &_data; }; template struct ConstLock { ConstLock(Volatile const &data) : _data (data) { _data._mutex.lock(); } ~ConstLock() { _data._mutex.unlock(); } T const& operator*() { return _data._data; } T const* operator->() { return &**this; } private: Volatile 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 *ressortingction) const { std::cout << "do action " << restriction << '\n'; } }; int main() { Volatile x; Volatile const c; x->action(); c->action(); { Lock locked (x); locked->action(); } { ConstLock locked (x); // ConstLock from non-const object locked->action(); } { ConstLock locked (c); locked->action(); } return 0; }
Compare class Quelque chose à ce que l’utilisation d’Alexandrescu exigerait:
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 *ressortingction) const volatile { std::cout << "do action " << restriction << '\n'; } };
Regardez cela sous un angle différent. Lorsque vous déclarez une variable en tant que const, vous indiquez au compilateur que la valeur ne peut pas être modifiée par votre code. Mais cela ne signifie pas que la valeur ne changera pas . Par exemple, si vous faites cela:
const int cv = 123; int* that = const_cast(&cv); *that = 42;
… cela évoque un comportement indéfini selon la norme, mais dans la pratique, il se passera quelque chose. Peut-être que la valeur sera modifiée. Peut-être y aura-t-il un sigfault. Peut-être que le simulateur de vol sera lancé – qui sait Le fait est que vous ne savez pas, indépendamment de la plate-forme, ce qui va se passer. Donc, la promesse apparente de const
n’est pas remplie. La valeur peut être réelle ou non.
Maintenant, étant donné que cela est vrai, utiliser const
un abus de langage? Bien sûr que non. C’est toujours un outil fourni par le langage pour vous aider à écrire un meilleur code. Ce ne sera jamais l’outil ultime pour s’assurer que les valeurs restnt inchangées – le cerveau du programmeur est finalement cet outil – mais est-ce que cela le rend inutile?
Je dis non, utiliser const comme outil pour vous aider à écrire un meilleur code n’est pas un abus de langage. En fait, j’irais un peu plus loin et dirais que c’est l’ intention de cette fonctionnalité.
Maintenant, la même chose est vraie de volatile. Déclarer que quelque chose est volatile ne rendra pas votre thread de programme sûr. Cela ne rendra probablement même pas cette variable ou cet object thread sûr. Mais le compilateur appliquera la sémantique de qualification de CV et un programmeur avisé pourra tirer parti de ce fait pour l’aider à écrire un meilleur code en aidant le compilateur à identifier les endroits où il pourrait écrire un bogue. Tout comme le compilateur l’aide quand il essaie de le faire:
const int cv = 123; cv = 42; // ERROR - comstackr complains that the programmer is potentially making a mistake
Oubliez les barrières de mémoire et l’atsortingcité d’objects et de variables volatiles, tout comme vous avez oublié depuis longtemps la véritable constance de cv
. Mais utilisez les outils fournis par le langage pour écrire un meilleur code. L’un de ces outils est volatile
.
Tu ferais mieux de ne pas faire ça. volatile n’a même pas été inventé pour assurer la sécurité des threads. Il a été inventé pour accéder correctement aux registres matériels mappés en mémoire. Le mot clé volatile n’a aucun effet sur la fonctionnalité d’exécution dans le désordre du processeur. Vous devez utiliser les appels de système d’exploitation appropriés ou les instructions CAS définies par la CPU, les barrières de mémoire, etc.
CAS
Barrière de mémoire
Dans l’article, le mot clé est utilisé davantage comme une balise
required_thread_safety
que pour l’utilisation réelle de volatile.
Sans avoir lu l’article, pourquoi Andrei n’utilise-t-il pas ladite balise required_thread_safety
alors? Abuser de volatile
ne semble pas être une si bonne idée ici. Je crois que cela cause davantage de confusion (comme vous l’avez dit), plutôt que de l’éviter.
Cela dit, volatile
certaine volatile
peut parfois être requirejse dans le code multithread même si cela n’est pas une condition suffisante pour empêcher le compilateur d’optimiser les vérifications qui reposent sur la mise à jour asynchrone d’une valeur.
Je ne sais pas précisément si le conseil d’Alexandrescu est valable, mais malgré tout, je le respecte comme un type très intelligent, son traitement de la sémantique de volatile révèle qu’il est sorti de son domaine de compétence. Volatile n’a absolument aucune valeur pour le multithreading (voir ici pour un bon traitement du sujet) et donc l’affirmation d’Alexandrescu selon laquelle «volatile est utile pour l’access multithread» m’amène à me demander sérieusement combien de confiance je peux placer dans le rest de son article.