2010-10-05 4 views
5

En faisant un montage dans une classe avec un long historique, j'ai été bloqué par une habitude particulière de l'architecte d'encapsuler sa séquence va_start -> va_end dans un mutex. Le changelog pour cette addition (qui a été faite il y a une quinzaine d'années, et non révisé depuis) ​​a noté que c'était parce que va_start et. tout n'était pas réentrant.Va_start (etc.) est-il réentrant?

Je n'étais pas au courant de ces problèmes avec va_start, car j'ai toujours pensé que c'était juste une macro pour certains calculs de pile-pointeur. Y a-t-il quelque chose que je ne connais pas? Je ne veux pas changer ce code s'il y a des effets secondaires.

Plus précisément, la fonction en question ressemble beaucoup à ceci:

void write(const char *format, ...) 
{ 
    mutex.Lock(); 
    va_list args; 
    va_start(args, format); 
    _write(format, args); 
    va_end(args); 
    mutex.Unlock(); 
} 

Ceci est appelé à partir de plusieurs threads.

+0

Si les verrous de commande _write l'IO série à un niveau octet ou niveau en mémoire tampon, vous pouvez toujours vouloir avoir un verrou de niveau supérieur pour rendre l'écriture() appellent plus atomique. Il n'y a rien de plus énervant à avoir 2 threads appellent 'printf (" foo ")' et 'printf (" bar ")' et obtenir '" fboaor "' sur votre sortie, au lieu de '' foobar ''ou' 'barfoo' ' . –

Répondre

5

En ce qui concerne la réentrée en série (c.-à-d., si foo() utilise va_start est-il sûr pour foo() d'appeler bar() qui utilise également va_start), la réponse est très bien - tant que l'instance va_list n'est pas la même chose. La norme dit:

Ni la macro va_start ni la macro va_copy ne doivent être appelées pour réinitialiser ap sans invocation intermédiaire de la macro va_end pour le même ap.

Donc, vous êtes OK aussi longtemps que l'on utilise un va_list différent (appelé ci-dessus ap).

Si par réentrée vous dire thread-safe (que je suppose que vous êtes, depuis mutex sont impliqués), vous aurez besoin de regarder à la mise en œuvre pour les détails. Puisque le standard C ne parle pas de multi-thread, ce problème dépend vraiment de l'implémentation. J'imagine qu'il pourrait être difficile de faire du va_start thread-safe sur certaines architectures bizarres ou de petite taille, mais je pense que si vous travaillez sur une plate-forme grand public moderne, vous n'aurez probablement aucun problème.

Sur les plates-formes plus traditionnelles tant comme un argument différent va_list est passé à la macro que vous va_start ne devrait avoir aucun problème avec plusieurs threads passant par le « même » va_start. Et puisque l'argument va_list est généralement sur la pile (et donc différents threads auront des instances différentes), vous avez généralement affaire à différentes instances du va_list.

Je pense que dans votre exemple, les mutex ne sont pas nécessaires pour l'utilisation varargs. Cependant, si le write(), il serait certainement judicieux pour un appel write() à sérialisé afin que vous n'avez pas plusieurs write() threads plissant sortie de l'autre.

+0

A propos de l'appel sérialisé - Oui, un mutex appartient ici, je voulais juste le mettre à un niveau inférieur. Il était à l'origine à un niveau inférieur, mais ce commit il y a 15 ans l'a déplacé là où vous le voyez, affirmant que le mutex avait aussi besoin de protéger le va_list, ce que je ne croyais pas! – Nate

2

Eh bien, la façon dont l'accès à la variable d'argument est implémentée en C rend plutôt évident que les objets va_list stockent certains état interne. Cela le rend non réentrant, ce qui signifie qu'appeler va_start sur un objet va_list annulerait l'effet du précédent va_start. Mais encore plus précisément, C explicitement interdit invoquant à nouveau va_start sur un objet va_list avant de "fermer" la session précédemment appelée va_start avec va_end.

Un objet va_list est censé être utilisé de manière "sans chevauchement": va_start...va_end. Après cela, vous pouvez faire un autre va_start sur le même objet va_list. Mais essayer de chevaucher les sessions va_start...va_end sur le même objet va_list ne fonctionnera pas.

P.S. En fait, il est théoriquement possible d'implémenter un état interne basé sur LIFO dans n'importe quel itérateur basé sur une session. C'est à dire. il est théoriquement possible d'autoriser des sessions va_start...va_end imbriquées sur le même objet va_list (le rendant ainsi réentrant dans ce sens). Mais la spécification de la bibliothèque C ne fournit rien de tel. Notez cependant que dans C99 va_list les objets sont copiables par va_copy

Donc, si vous avez besoin de parcourir la même liste d'arguments par plusieurs sessions va_start...va_end se chevauchant, vous pouvez toujours réaliser cela en créant plusieurs copies indépendantes de l'original va_list.

P.P.S. En regardant l'exemple de code que vous avez fourni ... Il n'y a absolument aucun besoin de mutex dans ce cas (en ce qui concerne l'intégrité de va_list). Et il n'y a pas besoin d'un objet va_list réentrant. Vous codez parfaitement bien sans aucun mutex. Cela fonctionnera correctement dans un environnement multi-thread. Les macros du groupe va_... ne fonctionnent pas sur le "pointeur de pile" réel. Au lieu de cela, ils fonctionnent sur un objet indépendant complet pouvant être utilisé pour itérer sur les valeurs stockées dans la pile. Vous pouvez le considérer comme votre propre pointeur de pile local privé. Chaque thread invoquant votre fonction recevra sa propre copie de ce va_list en itérant sur sa propre pile. Il n'y aura pas de conflit entre les threads.

+1

Oui, deux appels de va_start sur un seul va_list invalideraient cette liste. Ainsi, pour un va_list statique (ou global, ou un membre de la classe ...), il serait peut-être invalidé, mais un appel futur à la même fonction ferait une nouvelle va_list. – Nate

+0

@Nate: Oui, mais on n'a pas forcément besoin d'un 'va_list' statique pour rencontrer le problème. Des probl'emes de ce genre surviennent habituellement lorsque l'on op'ere sur la même 'va_list' locale'a partir d'un même appel'a la même fonction variadique. Il n'y a rien d'illégal à ce sujet. – AnT

0

Il y a des plates-formes où va_list aurait des problèmes de réentrance, mais sur cette même plate-forme toutes variables locales ont ces problèmes. Je suis curieux, cependant: qu'est-ce que votre fonction _write s'attend à faire? Si elle utilise des paramètres qui sont configurés avant d'appeler write, cela peut causer des problèmes de threads à moins que (1) une instance particulière de l'objet contenant _write ne soit utilisée par un thread à la fois, ou (2) Les threads utilisant un objet à _write veulent les mêmes paramètres de configuration.

+0

_write sera toujours protégé. Je voulais juste réduire la protection à ce niveau, et ne pas avoir à me soucier de tous les pointeurs de va_list. – Nate