En C++, les atomiques peuvent-ils subir des stockages erronés ?
Par exemple, supposons que m
y n
sont des atomiques et que m = 5
initialement. Dans le fil 1,
m += 2;
Dans le fil 2,
n = m;
Résultat : la valeur finale de n
devrait être soit 5 soit 7, non ? Mais est-ce que ça pourrait être faussement 6 ? Pourrait-il s'agir de 4 ou de 8, ou même d'autre chose ?
En d'autres termes, le modèle de mémoire C++ interdit-il au thread 1 de se comporter comme s'il avait fait cela ?
++m;
++m;
Ou, plus bizarrement, comme si c'était le cas ?
tmp = m;
m = 4;
tmp += 2;
m = tmp;
Référence : H.-J. Boehm et S. V. Adve, 2008, Figure 1. (Si vous suivez le lien, alors, dans la section 1 du document, voyez le premier élément à puces : "Les spécifications informelles fournies par ...")
LA QUESTION SOUS FORME ALTERNÉE
Une réponse (appréciée) montre que la question ci-dessus peut être mal comprise. Si cela est utile, voici la question sous une autre forme.
Supposons que le programmeur ait essayé de dire au thread 1 à sauter l'opération :
bool a = false;
if (a) m += 2;
Le modèle de mémoire C++ interdit-il au thread 1 de se comporter, au moment de l'exécution, comme s'il avait fait cela ?
m += 2; // speculatively alter m
m -= 2; // oops, should not have altered! reverse the alteration
Je pose la question parce que Boehm et Adve, dont les liens ont été cités précédemment, semblent expliquer qu'une exécution multithread peut
- modifier de façon spéculative une variable, mais ensuite
- modifier ultérieurement la variable pour lui redonner sa valeur initiale lorsque l'altération spéculative s'avère inutile.
EXEMPLE DE CODE COMPILABLE
Voici un code que vous pouvez compiler, si vous le souhaitez.
#include <iostream>
#include <atomic>
#include <thread>
// For the orignial question, do_alter = true.
// For the question in alternate form, do_alter = false.
constexpr bool do_alter = true;
void f1(std::atomic_int *const p, const bool do_alter_)
{
if (do_alter_) p->fetch_add(2, std::memory_order_relaxed);
}
void f2(const std::atomic_int *const p, std::atomic_int *const q)
{
q->store(
p->load(std::memory_order_relaxed),
std::memory_order_relaxed
);
}
int main()
{
std::atomic_int m(5);
std::atomic_int n(0);
std::thread t1(f1, &m, do_alter);
std::thread t2(f2, &m, &n);
t2.join();
t1.join();
std::cout << n << "\n";
return 0;
}
Ce code imprime toujours 5
ou 7
quand je l'exécute. (En fait, pour autant que je puisse dire, il imprime toujours 7
quand je l'exécute). Cependant, je ne vois rien dans la sémantique qui l'empêcherait d'imprimer 6
, 4
ou 8
.
L'excellent Cppreference.com États, "Les objets atomiques sont exempts de courses de données", ce qui est bien, mais dans un tel contexte, qu'est-ce que cela signifie ?
Sans doute, tout cela signifie que je ne comprends pas très bien la sémantique. Tout éclairage que vous pouvez apporter sur la question serait apprécié.
RÉPONSES
@Christophe, @ZalmanStern et @BenVoigt éclairent chacun la question avec talent. Leurs réponses coopèrent plutôt que de se concurrencer. À mon avis, les lecteurs devraient tenir compte des trois réponses : @Christophe d'abord ; @ZalmanStern ensuite ; et @BenVoigt enfin pour résumer.
4 votes
Même pour l'addition non atomique et l'affectation, une variable entière ne peut pas obtenir de valeurs "parasites" comme celles qui vous inquiètent. Et (en pratique) cela vaut même pour les éventuelles courses de données que vous pourriez avoir. Une opération comme
m += 2
est égal àm = m + 2
et non pas deux incréments consécutifs de un.4 votes
Et pourquoi demandez-vous cela ? Avez-vous un réel problème qui conduit à cette question ? Vous devriez peut-être vous interroger sur que à la place ?
10 votes
Non, ils ne peuvent pas. C'est ce que signifie "atomique". Indivisible, ne peut pas observer de parties séparées.
1 votes
@Someprogrammerdude : Non, je n'ai aucun problème réel. Je suis en train de lire les chapitres de Stroustrup et Williams sur les primitives de concurrence de bas niveau et la programmation sans verrou. Je lis également le livre de Holzmann sur le model checking concurrent, et j'ai lu l'article de Boehm et Adve, comme indiqué dans le lien. J'essaie de comprendre ce que je lis. C'est tout.
0 votes
@nwp : C'est ce que je pensais, mais ensuite j'ai lu l'article de Boehm et Adve, comme indiqué en lien. Maintenant, je ne suis plus aussi sûr. C'est pourquoi je pose la question.
2 votes
Si l'atomique le permettait, ils seraient inutiles. Donc non.
1 votes
@thb d'ailleurs, vous pouvez observer plus de résultats en ajoutant un peu de hasard dans l'exécution ( Démonstration en ligne ). Mais vous ne découvrirez que 5 et 7 ;-)
1 votes
Notez que le livre est antérieur au support des threads en c++.
3 votes
@Someprogrammerdude : Pour les opérations non volatiles non atomiques, les variables, y compris les variables entières de toutes tailles, peuvent très certainement contenir des valeurs intermédiaires parasites. Les écritures déchirées en sont l'exemple le plus évident, mais les écritures spéculatives telles que mentionnées dans la question sont également possibles. Les opérations atomiques et volatiles évitent ces stockages de valeurs parasites. (Mais attention, alors que la lecture et l'écriture sont atomiques pour les systèmes
volatile
les opérations de lecture-modification-écriture ne le sont pas)3 votes
@Someprogrammerdude : Il est tout à fait possible qu'un code multithread ait des bugs qui se manifesteront très, très, très rarement dans la vie réelle. Rarement, mais pas jamais. Je préfère de loin le code où je connaître qu'il fonctionne au code que je n'ai pas vu échouer (encore).
0 votes
@BenVoigt Quelles opérations volatiles sont garanties comme étant atomiques ? Toutes les écritures ?
0 votes
@curiousguy :
volatile
Les accès à la mémoire doivent générer exactement la même séquence d'accès à la mémoire que celle que l'on trouve dans le code. Ainsi, les lectures et les écritures doivent être atomiques (les opérations qui ne peuvent pas être rendues atomiques, telles que les opérations surdimensionnées, doivent échouer dans une implémentation totalement conforme), et les incréments, les décréments, les affectations composées doivent effectuer des opérations de lecture et d'écriture séparées, exactement une de chaque.0 votes
@BenVoigt Qu'en est-il des structures volatiles en C ? La modification doit-elle être atomique ?
0 votes
@Someprogrammerdude " une variable entière ne peut pas obtenir de valeurs "parasites" comme celles qui vous inquiètent. " Comment voulez-vous détecter de telles valeurs erronées ?
0 votes
@curiousguy Parce que
int
était la longueur "naturelle" du monde, et sur les processeurs Intel, il s'agit toujours d'une opération atomique pour écrire dans une longueur "naturelle" (ou plus petit ! ) mot. Pas seulement sur les processeurs Intel d'ailleurs. Je ne connais pas de cualquier architecture qui ne dispose pas d'écritures atomiques sur les entiers de taille de mot "naturelle" (ou plus petite).2 votes
@Someprogrammerdude Je suis d'accord pour dire que l'écriture à
int
est généralement atomique ; mais commeint
est défini par le compilateur et non par le CPU, la question ne porte pas sur l'architecture physique mais sur l'environnement de programmation.1 votes
@curiousguy :
volatile
sur une structure finit vraiment par modifier les accès aux membres. Chaque accès membre devrait être volatile, il n'y a pas d'opérations sur des structures entières en C. Par exemple, la copie est définie comme une séquence d'opérations membres. Je ne suis pas sûr que l'ordre soit spécifié, si c'est le cas, la copie non volatile des membres pourrait être réordonnée mais la copie volatile de la structure préserverait l'ordre.0 votes
@Someprogrammerdude Vous vouliez sûrement dire aligné écrire.