Le cout est-il synchronisé / thread-safe?

112

En général, je suppose que les flux ne sont pas synchronisés, c'est à l'utilisateur de faire le verrouillage approprié. Cependant, des choses comme coutobtenir un traitement spécial dans la bibliothèque standard?

Autrement dit, si plusieurs threads écrivent, coutpeuvent-ils corrompre l' coutobjet? Je comprends que même si synchronisé, vous obtiendrez toujours une sortie entrelacée de manière aléatoire, mais cet entrelacement est-il garanti? Autrement dit, est-il sûr de l'utiliser à coutpartir de plusieurs threads?

Ce fournisseur dépend-il? Que fait gcc?


Important : Veuillez fournir une sorte de référence pour votre réponse si vous dites «oui» car j'ai besoin d'une sorte de preuve de cela.

Ma préoccupation ne concerne pas non plus les appels système sous-jacents, ce sont bien, mais les flux ajoutent une couche de tampon par-dessus.

edA-qa mort-ora-y
la source
2
Cela dépend du fournisseur. C ++ (avant C ++ 0x) n'a aucune notion de plusieurs threads.
Sven
2
Qu'en est-il de C ++ 0x? Il définit un modèle de mémoire et ce qu'est un thread, alors peut-être que ces choses ont coulé dans la sortie?
rubenvb
2
Existe-t-il des fournisseurs qui le rendent thread-safe?
edA-qa mort-ora-y
Quelqu'un a-t-il un lien vers la dernière norme proposée C ++ 2011?
edA-qa mort-ora-y
4
Dans un certain sens, c'est là que printfbrille car la sortie complète est écrite stdouten un seul coup; lors de l'utilisation de std::coutchaque maillon de la chaîne d'expression, il serait émis séparément vers stdout; entre eux, il peut y avoir un autre thread d'écriture à stdoutcause duquel l'ordre de la sortie finale est perturbé.
legends2k

Réponses:

106

Le standard C ++ 03 n'en dit rien. Lorsque vous n'avez aucune garantie sur la sécurité des threads de quelque chose, vous devez le traiter comme non thread-safe.

Le fait qu'il coutsoit tamponné est particulièrement intéressant ici . Même si les appels à write(ou quoi que ce soit qui accomplit cet effet dans cette implémentation particulière) sont garantis d'être mutuellement exclusifs, le tampon peut être partagé par les différents threads. Cela conduira rapidement à une corruption de l'état interne du flux.

Et même si l'accès au tampon est garanti pour les threads, que pensez-vous qu'il va se passer dans ce code?

// in one thread
cout << "The operation took " << result << " seconds.";

// in another thread
cout << "Hello world! Hello " << name << "!";

Vous voulez probablement que chaque ligne ici agisse dans l'exclusion mutuelle. Mais comment une implémentation peut-elle garantir cela?

En C ++ 11, nous avons certaines garanties. Le FDIS dit ce qui suit au §27.4.1 [iostream.objects.overview]:

L'accès simultané aux fonctions d'entrée (§27.7.2.1) et de sortie (§27.7.3.1) d'un objet iostream standard synchronisé (§27.5.3.4) standard formaté et non formaté ou à un flux C standard par plusieurs threads ne doit pas entraîner une course aux données (§ 1.10). [Remarque: les utilisateurs doivent toujours synchroniser l'utilisation simultanée de ces objets et flux par plusieurs threads s'ils souhaitent éviter les caractères entrelacés. - note de fin]

Ainsi, vous n'obtiendrez pas de flux corrompus, mais vous devez toujours les synchroniser manuellement si vous ne voulez pas que la sortie soit des déchets.

R. Martinho Fernandes
la source
2
Techniquement vrai pour C ++ 98 / C ++ 03, mais je pense que tout le monde le sait. Mais cela ne répond pas aux deux questions intéressantes: qu'en est-il de C ++ 0x? Qu'est-ce que les implémentations typiques réellement faire ?
Nemo
1
@ edA-qa mort-ora-y: Non, vous vous trompez. C ++ 11 définit clairement que les objets de flux standard peuvent être synchronisés et conserver un comportement bien défini, et non pas qu'ils le sont par défaut.
ildjarn
12
@ildjarn - Non, @ edA-qa mort-ora-y est correct. Tant que cout.sync_with_stdio()c'est vrai, utiliser coutpour sortir des caractères de plusieurs threads sans synchronisation supplémentaire est bien défini, mais uniquement au niveau des octets individuels. Ainsi, cout << "ab";et cout << "cd"exécuté dans différents threads peut générer acdb, par exemple, mais ne peut pas provoquer un comportement indéfini.
JohannesD
4
@JohannesD: Nous sommes d'accord là-dessus - il est synchronisé avec l'API C sous-jacente. Mon point est que ce n'est pas «synchronisé» d'une manière utile, c'est-à-dire qu'il faut encore une synchronisation manuelle s'ils ne veulent pas de données inutiles.
ildjarn
2
@ildjarn, je suis d'accord avec les données sur les ordures, ce peu que je comprends. Je m'intéresse simplement à la condition de concurrence des données, qui semble être claire maintenant.
edA-qa mort-ora-y
16

c'est une excellente question.

Premièrement, C ++ 98 / C ++ 03 n'a pas de concept de "thread". Donc, dans ce monde, la question n'a pas de sens.

Qu'en est-il de C ++ 0x? Voir la réponse de Martinho (qui, je l'avoue, m'a surpris).

Qu'en est-il des implémentations spécifiques pré-C ++ 0x? Eh bien, par exemple, voici le code source basic_streambuf<...>:sputcde GCC 4.5.2 (en-tête "streambuf"):

 int_type
 sputc(char_type __c)
 {
   int_type __ret;
   if (__builtin_expect(this->pptr() < this->epptr(), true)) {
       *this->pptr() = __c;
        this->pbump(1);
        __ret = traits_type::to_int_type(__c);
      }
    else
        __ret = this->overflow(traits_type::to_int_type(__c));
    return __ret;
 }

De toute évidence, cela n'effectue aucun verrouillage. Et non plus xsputn. Et c'est certainement le type de streambuf que cout utilise.

Pour autant que je sache, libstdc ++ n'effectue aucun verrouillage autour des opérations de flux. Et je ne m'attendrais à aucun, car ce serait lent.

Donc, avec cette implémentation, il est évidemment possible pour la sortie de deux threads de se corrompre ( pas seulement d'entrelacer).

Ce code pourrait-il corrompre la structure de données elle-même? La réponse dépend des interactions possibles de ces fonctions; par exemple, que se passe-t-il si un thread essaie de vider le tampon tandis qu'un autre essaie d'appeler xsputnou autre. Cela peut dépendre de la manière dont votre compilateur et votre CPU décident de réorganiser les charges et les magasins de mémoire; il faudrait une analyse minutieuse pour être sûr. Cela dépend également de ce que fait votre CPU si deux threads tentent de modifier le même emplacement simultanément.

En d'autres termes, même si cela fonctionne correctement dans votre environnement actuel, il peut se casser lorsque vous mettez à jour l'un de vos runtime, compilateur ou CPU.

Résumé: "Je ne le ferais pas". Créez une classe de journalisation qui effectue un verrouillage approprié ou passez à C ++ 0x.

Comme alternative faible, vous pouvez définir cout sur non tamponné. Il est probable (bien que non garanti) que toute logique liée à la mémoire tampon soit ignorée et appelée writedirectement. Bien que cela puisse être d'une lenteur prohibitive.

Nemo
la source
1
Bonne réponse, mais regardez la réponse de Martinho qui montre que C ++ 11 définit la synchronisation pour cout .
edA-qa mort-ora-y
7

La norme C ++ ne spécifie pas si l'écriture dans les flux est thread-safe, mais ce n'est généralement pas le cas.

www.techrepublic.com/article/use-stl-streams-for-easy-c-plus-plus-thread-safe-logging

et aussi: les flux de sortie standard sont-ils en C ++ thread-safe (cout, cerr, clog)?

METTRE À JOUR

Veuillez consulter la réponse de @Martinho Fernandes pour savoir ce que le nouveau standard C ++ 11 en dit.

phoxis
la source
3
Je suppose que puisque C ++ 11 est maintenant la norme, cette réponse est en fait erronée maintenant.
edA-qa mort-ora-y
6

Comme d'autres réponses le mentionnent, cela est certainement spécifique au fournisseur puisque la norme C ++ ne fait aucune mention de threading (cela change dans C ++ 0x).

GCC ne fait pas beaucoup de promesses sur la sécurité des threads et les E / S. Mais la documentation de ce qu'il promet est ici:

l'essentiel est probablement:

Le type __basic_file est simplement une collection de petits wrappers autour de la couche C stdio (encore une fois, voir le lien sous Structure). Nous ne nous verrouillons pas, mais passons simplement par des appels à fopen, fwrite, et ainsi de suite.

Ainsi, pour la version 3.0, la question «est-ce que le multithreading est sûr pour les E / S» doit être répondu par «est-ce que la bibliothèque C de votre plate-forme est-elle threadsafe pour les E / S? Certains le sont par défaut, d'autres non; beaucoup offrent plusieurs implémentations de la bibliothèque C avec divers compromis entre sécurité et efficacité des threads. Vous, le programmeur, devez toujours vous occuper de plusieurs threads.

(Par exemple, le standard POSIX exige que les opérations C stdio FILE * soient atomiques. Les bibliothèques C conformes à POSIX (par exemple, sous Solaris et GNU / Linux) ont un mutex interne pour sérialiser les opérations sur les FILE *. Cependant, vous avez toujours besoin pour ne pas faire des choses stupides comme appeler fclose (fs) dans un thread suivi d'un accès de fs dans un autre.)

Ainsi, si la bibliothèque C de votre plate-forme est threadsafe, vos opérations d'E / S fstream seront threadsafe au niveau le plus bas. Pour les opérations de niveau supérieur, comme la manipulation des données contenues dans les classes de formatage de flux (par exemple, la mise en place de rappels dans un std :: ofstream), vous devez protéger ces accès comme toute autre ressource partagée critique.

Je ne sais pas si quelque chose a changé depuis le délai 3.0 mentionné.

La documentation de sécurité des threads de MSVC pour iostreamspeut être trouvée ici: http://msdn.microsoft.com/en-us/library/c9ceah3b.aspx :

Un seul objet est thread-safe pour la lecture à partir de plusieurs threads. Par exemple, étant donné un objet A, il est sûr de lire A à partir du thread 1 et du thread 2 simultanément.

Si un seul objet est en cours d'écriture par un thread, toutes les lectures et écritures sur cet objet sur le même thread ou sur d'autres threads doivent être protégées. Par exemple, étant donné un objet A, si le thread 1 écrit dans A, alors le thread 2 doit être empêché de lire ou d'écrire dans A.

Il est sûr de lire et d'écrire dans une instance d'un type même si un autre thread lit ou écrit dans une instance différente du même type. Par exemple, étant donné les objets A et B du même type, il est sûr si A est écrit dans le thread 1 et B est lu dans le thread 2.

...

Classes iostream

Les classes iostream suivent les mêmes règles que les autres classes, à une exception près. Il est sûr d'écrire sur un objet à partir de plusieurs threads. Par exemple, le thread 1 peut écrire dans cout en même temps que le thread 2. Toutefois, cela peut entraîner le mélange de la sortie des deux threads.

Remarque: la lecture à partir d'un tampon de flux n'est pas considérée comme une opération de lecture. Il doit être considéré comme une opération d'écriture, car cela modifie l'état de la classe.

Notez que ces informations concernent la version la plus récente de MSVC (actuellement pour VS 2010 / MSVC 10 / cl.exe16.x). Vous pouvez sélectionner les informations des anciennes versions de MSVC à l'aide d'un contrôle déroulant sur la page (et les informations sont différentes pour les anciennes versions).

Michael Burr
la source
1
"Je ne sais pas si quelque chose a changé depuis la période 3.0 mentionnée." C'était définitivement le cas. Au cours des dernières années, l'implémentation des flux g ++ a effectué sa propre mise en mémoire tampon.
Nemo le