Utilisation de C / Pthreads: les variables partagées doivent-elles être volatiles?

StackOverflow https://stackoverflow.com/questions/78172

  •  09-06-2019
  •  | 
  •  

Question

Dans le langage de programmation C et Pthreads comme bibliothèque de threading; les variables / structures partagées entre les threads doivent-elles être déclarées volatiles? En supposant qu'ils puissent être protégés ou non par un verrou (des barrières peut-être).

La norme POSIX de pthread a-t-elle son mot à dire, cela dépend-il du compilateur ou de l'un ou l'autre?

Modifier pour ajouter: Merci pour les bonnes réponses. Mais que se passe-t-il si vous n'utilisez pas de verrous; Et si vous utilisiez par exemple obstacles ? Ou du code utilisant des primitives telles que comparer-échanger pour modifier directement et de manière atomique une variable partagée. ..

Était-ce utile?

La solution

Je pense qu’une propriété très importante de volatile est qu’elle permet d’écrire la variable en mémoire lors de la modification et de la relire à partir de la mémoire à chaque accès. Les autres réponses combinent volatilité et synchronisation, et il ressort clairement d’autres réponses que volatile n’est PAS une primitive de synchronisation (crédit lorsque le crédit est dû).

Mais à moins que vous n'utilisiez volatile, le compilateur est libre de mettre en cache les données partagées dans un registre pour une durée indéterminée ... si vous souhaitez que vos données soient écrites de manière prévisible dans la mémoire réelle et pas uniquement inscrivez-vous par le compilateur à sa discrétion, vous devrez le marquer comme volatil. Sinon, si vous n’accédez aux données partagées qu’après avoir quitté une fonction qui les modifie, vous vous en sortirez peut-être. Mais je suggérerais de ne pas miser sur la chance aveugle pour s’assurer que les valeurs sont écrites des registres dans la mémoire.

Surtout sur les machines riches en registres (c'est-à-dire pas x86), les variables peuvent vivre assez longtemps dans les registres, et un bon compilateur peut mettre en cache même des parties de structures ou des structures entières dans des registres. Vous devez donc utiliser volatile, mais pour des performances optimales, copiez également des valeurs dans des variables locales à des fins de calcul, puis effectuez une écriture explicite. Essentiellement, utiliser efficacement la méthode volatile signifie réfléchir un peu au stockage en charge dans votre code C.

Dans tous les cas, vous devez absolument utiliser un mécanisme de synchronisation fourni par le système d'exploitation pour créer un programme correct.

Pour un exemple de la faiblesse de volatile, voir l’exemple de mon algorithme Decker à l'adresse http: //jakob.engbloms. .se / archives / 65 , ce qui prouve assez bien que volatile ne fonctionne pas pour se synchroniser.

Autres conseils

Tant que vous utilisez des verrous pour contrôler l'accès à la variable, vous n'avez pas besoin de volatile. En fait, si vous mettez volatile sur une variable, vous vous trompez probablement déjà.

https://software.intel.com/en-us/blogs/2007/11/30/volatile-almost-almost-useless-for-multi-threaded- programmation /

La réponse est absolument, sans équivoque, NON. Vous n'avez pas besoin d'utiliser 'volatile' en plus des primitives de synchronisation appropriées. Tout ce qui doit être fait est fait par ces primitives.

L’utilisation de «volatile» n’est ni nécessaire ni suffisante. Ce n'est pas nécessaire car les primitives de synchronisation appropriées sont suffisantes. Ce n'est pas suffisant car cela ne désactive que certaines optimisations, pas toutes celles qui risquent de vous mordre. Par exemple, cela ne garantit ni l’atmicité ni la visibilité sur un autre processeur.

  

Mais à moins que vous n'utilisiez volatile, le compilateur est libre de mettre en cache les données partagées dans un registre pour une durée indéterminée ... si vous souhaitez que vos données soient écrites de manière prévisible dans la mémoire réelle et pas uniquement inscrivez-vous par le compilateur à sa discrétion, vous devrez le marquer comme volatil. Sinon, si vous n’accédez aux données partagées qu’après avoir quitté une fonction qui les modifie, vous vous en sortirez peut-être. Mais je suggérerais de ne pas miser sur la chance aveugle pour s’assurer que les valeurs sont écrites des registres dans la mémoire.

Bien, mais même si vous utilisez volatile, la CPU est libre de mettre en cache les données partagées dans un tampon d’écriture pour une durée indéterminée. L’ensemble des optimisations qui peuvent vous mordre n’est pas exactement le même que celui des optimisations désactivées par «volatile». Donc, si vous utilisez 'volatile', vous reposez sur une chance aveugle.

D'autre part, si vous utilisez des primitives de synchronisation avec une sémantique multi-thread définie, vous avez la garantie que tout fonctionnera. En plus, vous ne prenez pas l'énorme succès de «volatile». Alors, pourquoi ne pas faire les choses de cette façon?

Il existe une notion répandue selon laquelle le mot clé volatile est utile pour la programmation multithread.

Hans Boehm souligne qu'il n'y a que trois utilisations portables pour volatile:

  • volatile peut être utilisé pour marquer des variables locales dans la même portée qu'un setjmp dont la valeur doit être préservée sur un longjmp. Il est difficile de savoir quelle fraction de telles utilisations serait ralentie, car les contraintes d'atomicité et d'ordonnancement n'ont aucun effet s'il n'existe aucun moyen de partager la variable locale en question. (On ne sait même pas quelle fraction de telles utilisations serait ralentie en exigeant que toutes les variables soient préservées sur une longueur, mais ceci est une question distincte et n’est pas considéré ici.)
  • volatile peut être utilisé lorsque des variables peuvent être "modifiées de manière externe", mais la modification est en fait déclenchée de manière synchrone par le thread lui-même, par ex. car la mémoire sous-jacente est mappée à plusieurs emplacements.
  • Un sigatomic_t volatile peut être utilisé pour communiquer avec un gestionnaire de signaux dans le même thread, de manière restreinte. On pourrait envisager d’affaiblir les exigences relatives au cas sigatomic_t, mais cela semble plutôt contre-intuitif.

Si vous êtes multi-threading pour des raisons de rapidité, ralentir le code n'est certainement pas ce que vous souhaitez. Pour la programmation multithread, on pense souvent, à tort, que l’on suppose que volatile est résolu:

  • atomicité
  • cohérence de la mémoire , c’est-à-dire l’ordre des opérations d’un thread vu par un autre thread.

Parlons d'abord de (1). Volatile ne garantit pas les lectures ou écritures atomiques. Par exemple, une lecture ou une écriture volatile d'une structure de 129 bits ne sera pas atomique sur la plupart des matériels modernes. Une lecture ou une écriture volatile d'un int de 32 bits est atomique sur la plupart des matériels modernes, mais volatile n'a rien à voir avec cela . Ce serait probablement atomique sans le volatile. L'atomicité est à la merci du compilateur. Rien dans les normes C ou C ++ n’indique qu’il doit être atomique.

Considérons maintenant la question (2). Parfois, les programmeurs pensent que la volatilité désactive l'optimisation des accès volatils. C'est en grande partie vrai dans la pratique. Mais ce ne sont que les accès volatiles, pas les accès non volatiles. Considérez ce fragment:

 volatile int Ready;       

    int Message[100];      

    void foo( int i ) {      

        Message[i/10] = 42;      

        Ready = 1;      

    }

Il s’agit de faire quelque chose de très raisonnable en programmation multi-thread: écrire un message puis l’envoyer à un autre thread. L'autre thread attendra que Ready devienne différent de zéro, puis lira Message. Essayez de le compiler avec "gcc -O2 -S". en utilisant gcc 4.0, ou icc. Les deux font le magasin sur Prêt en premier, afin qu'il puisse être chevauché avec le calcul de i / 10. La réorganisation n'est pas un bug du compilateur. C'est un optimiseur agressif qui fait son travail.

Vous pourriez penser que la solution consiste à marquer toutes vos références de mémoire comme volatiles. C'est tout simplement idiot. Comme le disent les citations précédentes, cela ralentira simplement votre code. Pire encore, cela pourrait ne pas résoudre le problème. Même si le compilateur ne réorganise pas les références, le matériel peut le faire. Dans cet exemple, le matériel x86 ne le réorganisera pas. Aucun processeur Itanium (TM) non plus, car les compilateurs Itanium insèrent des barrières de mémoire pour les magasins volatiles. C'est une extension intelligente d'Itanium. Mais des puces comme Power (TM) seront réorganisées. Pour la commande, vous avez vraiment besoin de barrières de mémoire , également appelées barrières de mémoire . Une barrière de mémoire empêche le réordonnancement des opérations de mémoire sur la clôture ou, dans certains cas, le réordonne dans un seul sens. La volatilité n'a rien à voir avec les barrières de mémoire.

Alors, quelle est la solution pour la programmation multi-thread? Utilisez une bibliothèque ou une extension de langage qui implémente la sémantique atomique et la clôture. Lorsqu'elles sont utilisées comme prévu, les opérations de la bibliothèque insèrent les bonnes clôtures. Quelques exemples:

  • les threads POSIX
  • threads Windows (TM)
  • OpenMP
  • TBB

Basé sur article d'Arch Robison (Intel)

D'après mon expérience, non. il vous suffit de bien mutiler vous-même lorsque vous écrivez dans ces valeurs, ou de structurer votre programme de manière à ce que les threads s'arrêtent avant d'avoir besoin d'accéder à des données qui dépendent des actions d'un autre thread. Mon projet, x264, utilise cette méthode. Les threads partagent une énorme quantité de données, mais la grande majorité d'entre eux n'a pas besoin de mutex, car son thread en lecture seule ou un thread attendra que les données soient disponibles et finalisées avant de devoir y accéder.

Maintenant, si vous avez plusieurs threads qui sont tous fortement imbriqués dans leurs opérations (ils dépendent de la sortie de chacun sur un niveau très fin), cela peut être beaucoup plus difficile - en fait, dans un tel cas J'envisagerais de revoir le modèle de thread pour voir s'il est possible de le faire plus proprement avec davantage de séparation entre les threads.

NON.

Volatile n'est requis que pour la lecture d'un emplacement mémoire modifiable indépendamment des commandes de lecture / écriture de la CPU. En cas de threading, la CPU contrôle totalement les lectures / écritures en mémoire pour chaque thread. Le compilateur peut donc supposer que la mémoire est cohérente et optimiser les instructions de la CPU pour réduire les accès inutiles à la mémoire.

L'utilisation principale de volatile est l'accès aux E / S mappées en mémoire. Dans ce cas, le périphérique sous-jacent peut modifier la valeur d'un emplacement de mémoire indépendamment de la CPU. Si vous n'utilisez pas volatile dans cette condition, la CPU peut utiliser une valeur de mémoire précédemment mise en cache au lieu de lire la valeur récemment mise à jour.

Volatile ne serait utile que si vous n'avez absolument besoin d'aucun délai entre le moment où un thread écrit quelque chose et celui qu'un autre le lit. Sans une sorte de verrou, cependant, vous n'avez aucune idée de quand l'autre thread a écrit les données, mais que c'est la valeur la plus récente possible.

Pour les valeurs simples (int et float dans leurs différentes tailles), un mutex peut être excessif si vous n'avez pas besoin d'un point de synchronisation explicite. Si vous n'utilisez pas un mutex ou un verrou quelconque, vous devez déclarer la variable volatile. Si vous utilisez un mutex, vous êtes prêt.

Pour les types compliqués, vous devez utiliser un mutex. Les opérations sur eux ne sont pas atomiques, vous pouvez donc lire une version à moitié modifiée sans mutex.

Volatile signifie que nous devons aller en mémoire pour obtenir ou définir cette valeur. Si vous ne définissez pas volatile, le code compilé peut stocker les données dans un registre pendant une longue période.

Cela signifie que vous devez marquer les variables que vous partagez entre les threads comme volatiles afin d'éviter les situations dans lesquelles un thread commence à modifier la valeur mais n'écrit pas son résultat avant qu'un second thread ne se présente et tente de lire la valeur.

Volatile est un indice du compilateur qui désactive certaines optimisations. L'assemblage de sortie du compilateur aurait pu être sécurisé sans lui, mais vous devriez toujours l'utiliser pour les valeurs partagées.

Ceci est particulièrement important si vous n'utilisez PAS les coûteux objets de synchronisation de thread fournis par votre système. Vous pouvez par exemple avoir une structure de données où vous pouvez la maintenir valide avec une série de modifications atomiques. De nombreuses piles qui n'allouent pas de mémoire sont des exemples de telles structures de données, car vous pouvez ajouter une valeur à la pile, puis déplacer le pointeur de fin ou supprimer une valeur de la pile après avoir déplacé le pointeur de fin. Lors de la mise en place d'une telle structure, volatile devient crucial pour garantir que vos instructions atomiques le soient réellement.

La raison sous-jacente est que la sémantique du langage C est basée sur une machine abstraite à un seul thread . Et le compilateur est en droit de transformer le programme tant que les «comportements observables» du programme sur la machine abstraite restent inchangés. Il peut fusionner des accès mémoire adjacents ou qui se chevauchent, refaire un accès mémoire plusieurs fois (en cas de renversement de registre, par exemple) ou simplement ignorer un accès mémoire s'il considère les comportements du programme lorsqu'il est exécuté dans un seul thread . , ne change pas. Par conséquent, comme vous vous en doutez, les comportements changent si le programme est censé s’exécuter de manière multithread.

Comme Paul Mckenney l’a souligné dans un célèbre Document sur le noyau Linux :

  

Il ne faut pas supposer que le compilateur fera ce que vous voulez        avec des références de mémoire non protégées par READ_ONCE () et        WRITE_ONCE (). Sans eux, le compilateur est dans ses droits de        faire toutes sortes de " créatif " transformations, qui sont couvertes dans        la section COMPILER BARRIER.

READ_ONCE () et WRITE_ONCE () sont définis comme des distributions instables sur des variables référencées. Ainsi:

int y;
int x = READ_ONCE(y);

est équivalent à:

int y;
int x = *(volatile int *)&y;

Ainsi, à moins que vous ne créiez un accès "volatile", vous n'êtes pas assuré que cet accès se produit exactement une fois , quel que soit le mécanisme de synchronisation que vous utilisez. L'appel d'une fonction externe (pthread_mutex_lock par exemple) peut forcer le compilateur à accéder à la mémoire à des variables globales. Mais cela ne se produit que lorsque le compilateur ne parvient pas à déterminer si la fonction externe modifie ou non ces variables globales. Les compilateurs modernes utilisant une analyse inter-procédure sophistiquée et une optimisation du temps de liaison rendent cette astuce tout simplement inutile.

En résumé, vous devez marquer les variables partagées par plusieurs threads comme volatiles ou y accéder à l'aide de distributions instables.

Comme Paul McKenney l’a également souligné:

  

J'ai vu le reflet de leurs yeux lorsqu'ils discutent de techniques d'optimisation que vous ne voudriez pas que vos enfants connaissent!

Mais voyez ce qu'il advient de C11 / C ++ 11 .

Je ne comprends pas. Comment les primitives de synchronisation forcent-elles le compilateur à recharger la valeur d'une variable? Pourquoi n’utiliserait-il pas la dernière copie qu’il possède déjà?

Volatile signifie que la variable est mise à jour en dehors de la portée du code et que, par conséquent, le compilateur ne peut pas supposer qu'il en connaisse la valeur actuelle. Même les barrières de mémoire sont inutiles, car le compilateur, inconscient des barrières de mémoire (non?), Pourrait quand même utiliser une valeur en cache.

Certaines personnes supposent évidemment que le compilateur traite les appels de synchronisation comme des barrières de mémoire. " Casey " suppose qu'il y a exactement un processeur.

Si les primitives de synchronisation sont des fonctions externes et que les symboles en question sont visibles à l'extérieur de l'unité de compilation (noms globaux, pointeur exporté, fonction exportée susceptible de les modifier), le compilateur les traitera - ou tout autre appel de fonction externe - - en tant que barrière de mémoire vis-à-vis de tous les objets visibles de l'extérieur.

Sinon, vous êtes seul. Et volatile peut être le meilleur outil disponible pour que le compilateur produise un code correct et rapide. Cependant, il ne sera généralement pas portable, lorsque vous aurez besoin de la technologie volatile et que ce qu’il fait pour vous dépend en grande partie du système et du compilateur.

Non.

Tout d'abord, volatile n'est pas nécessaire. De nombreuses autres opérations fournissent une sémantique multithread garantie qui n'utilise pas volatile . Celles-ci incluent les opérations atomiques, les mutex, et ainsi de suite.

Deuxièmement, volatile n'est pas suffisant. La norme C ne fournit aucune garantie quant au comportement multithread des variables déclarées volatile .

Donc, n'étant ni nécessaire ni suffisant, il n'y a pas grand intérêt à l'utiliser.

Une exception serait les plates-formes particulières (telles que Visual Studio) où la sémantique multithread est documentée.

Les variables partagées entre les threads doivent être déclarées "volatiles". Cela raconte la le compilateur que lorsqu'un thread écrit dans de telles variables, l'écriture doit se faire en mémoire (par opposition à un registre).

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top