Tout d'abord, vous devez apprendre à penser comme un Langage Avocat.
Le C++ spécification ne fait pas référence à un compilateur, système d'exploitation, ou CPU. Il fait référence à une machine abstraite qui est une généralisation de systèmes réels. Dans la Langue de l'Avocat du monde, le travail du programmeur consiste à écrire du code pour la machine abstraite; le travail du compilateur est de concrétiser ce code sur un béton de la machine. Par le codage de manière rigide à la spec, vous pouvez être certain que votre code de compiler et de l'exécuter sans modification sur n'importe quel système avec un conforme compilateur C++, que ce soit aujourd'hui ou 50 ans à partir de maintenant.
La machine abstraite dans le C++98/C++03 spécification est fondamentalement single-threaded. Donc, il n'est pas possible d'écrire multi-threaded code C++ qui est "portable" à l'égard de la spec. La spec n'a même pas dit rien sur l' atomicité de la mémoire des charges et des magasins ou dans l' ordre dans lequel les charges et les magasins qui pourrait arriver, jamais l'esprit des choses comme les mutex.
Bien sûr, vous pouvez écrire le code multithread, dans la pratique, pour certains systèmes concrets-comme pthreads ou Windows. Mais il n'y a pas de standard façon d'écrire le code multithread pour C++98/C++03.
La machine abstraite en C++11 est multi-thread de par leur conception. Il dispose également d'un bien définis modèle de mémoire; c'est, dit-il ce que le compilateur peut et ne peut pas faire quand il s'agit de l'accès à la mémoire.
Prenons l'exemple suivant, où une paire de variables globales sont accessibles simultanément par deux fils:
Global
int x, y;
Thread 1 Thread 2
x = 17; cout << y << " ";
y = 37; cout << x << endl;
Ce qui pourrait Thread 2 sortie?
Sous C++98/C++03, ce n'est même pas un Comportement Indéfini; la question elle-même est vide de sens parce que la norme ne prévoit pas quelque chose appelé un "thread".
En vertu de C++11, le résultat est un Comportement Indéfini, parce que les charges et les magasins n'ont pas besoin d'être atomique en général. Qui peut ne pas sembler une amélioration... Et par lui-même, il n'est pas.
Mais avec le C++11, vous pouvez écrire ceci:
Global
atomic<int> x, y;
Thread 1 Thread 2
x.store(17); cout << y.load() << " ";
y.store(37); cout << x.load() << endl;
Maintenant les choses deviennent beaucoup plus intéressantes. Tout d'abord, le comportement est défini. Thread 2 peut maintenant imprimer 0 0
(si elle s'exécute avant que le Thread 1), en 37 17
(si il court après le Thread 1), ou 0 17
(si il court après le Thread 1 attribue à x, mais avant qu'il assigne à y).
Ce qu'il ne peut pas imprimer est - 37 0
, parce que le mode par défaut pour l'charges/magasins en C++11 est de mettre en cohérence séquentielle. Cela signifie simplement que toutes les charges et les magasins doivent être "comme si" ils s'est passé dans l'ordre que vous avez écrit dans chaque thread, tandis que les opérations entre les threads peuvent être entrelacées cependant, le système aime. Ainsi, le comportement par défaut de atomics fournit à la fois l'atomicité et de commande pour les charges et les magasins.
Maintenant, sur un PROCESSEUR récent, assurer la cohérence séquentielle peut être coûteux. En particulier, le compilateur est susceptible d'émettre de plein fouet les barrières de la mémoire entre chaque accès ici. Mais si votre algorithme peut tolérer de charges et magasins; c'est à dire, si elle nécessite d'atomicité, mais pas la commande; c'est à dire, si l'on peut tolérer 37 0
que la sortie de ce programme, alors vous pouvez écrire ceci:
Global
atomic<int> x, y;
Thread 1 Thread 2
x.store(17,memory_order_relaxed); cout << y.load(memory_order_relaxed) << " ";
y.store(37,memory_order_relaxed); cout << x.load(memory_order_relaxed) << endl;
Le plus moderne, le CPU, le plus probable c'est d'être plus rapide que dans l'exemple précédent.
Enfin, si vous avez juste besoin de garder certaines charges particulières et les stocke dans l'ordre, vous pouvez écrire:
Global
atomic<int> x, y;
Thread 1 Thread 2
x.store(17,memory_order_release); cout << y.load(memory_order_acquire) << " ";
y.store(37,memory_order_release); cout << x.load(memory_order_acquire) << endl;
Cela nous ramène à la commande, les charges et les magasins -- 37 0
n'est plus une sortie possible -- mais il le fait avec un minimum de frais généraux. (Dans cet exemple trivial, le résultat est le même que complet à la cohérence séquentielle; dans un programme plus large, elle ne serait pas.)
Bien sûr, si les seules sorties que vous voulez voir sont 0 0
ou 37 17
, vous pouvez juste envelopper un mutex à travers le code d'origine. Mais si vous avez lu jusqu'ici, je parie que vous savez déjà comment cela fonctionne, et cette réponse, c'est déjà plus long que j'ai prévu :-).
Donc, la ligne du bas. Les mutex sont grands, et le C++11 normalise. Mais parfois, pour des raisons de performance que vous voulez fonctions primitives de bas niveau (par exemple, le classique double-vérifier le verrouillage de modèle). La nouvelle norme fournit des gadgets comme les mutex et les variables de condition, et il fournit également un faible niveau de gadgets comme les types atomiques et les différentes saveurs de la mémoire de la barrière. Alors maintenant, vous pouvez écrire sophistiquée, hautes performances simultanées routines entièrement dans la langue spécifiée par la norme, et vous pouvez être certain que votre code de compiler et d'exécuter de manière identique sur les deux aujourd'hui et de demain.
Bien que, pour être franc, sauf si vous êtes un expert et de travail sur certaines graves au code de bas niveau, vous devriez probablement s'en tenir à mutex et les variables de condition. C'est ce que j'ai l'intention de le faire.
Pour en savoir plus sur ce genre de choses, voir ce billet de blog.