J'ai écrit un simple programme multithreading comme suit:
static bool finished = false;
int func()
{
size_t i = 0;
while (!finished)
++i;
return i;
}
int main()
{
auto result=std::async(std::launch::async, func);
std::this_thread::sleep_for(std::chrono::seconds(1));
finished=true;
std::cout<<"result ="<<result.get();
std::cout<<"\nmain thread id="<<std::this_thread::get_id()<<std::endl;
}
Il se comporte normalement en mode débogage dans Visual studio ou -O0
en gc c et imprime le résultat après 1
quelques secondes. Mais il est bloqué et n'imprime rien en mode Release ou -O1 -O2 -O3
.
c++
multithreading
thread-safety
data-race
sz ppeter
la source
la source
Réponses:
Deux threads, accédant à une variable non atomique et non gardée sont UB Cela concerne
finished
. Vous pouvez fairefinished
du typestd::atomic<bool>
pour résoudre ce problème.Ma solution:
Production:
Démo en direct sur coliru
Quelqu'un peut penser: «C'est un
bool
- probablement un peu. Comment cela peut-il être non atomique? ' (Je l'ai fait quand j'ai commencé avec le multi-threading moi-même.)Mais notez que le manque de déchirure n'est pas la seule chose qui
std::atomic
vous donne. Cela rend également l'accès en lecture + écriture simultané à partir de plusieurs threads bien défini, empêchant le compilateur de supposer que la relecture de la variable verra toujours la même valeur.La fabrication d'un
bool
non-atomique non protégé peut entraîner des problèmes supplémentaires:atomic<bool>
avecmemory_order_relaxed
magasin / charge fonctionnerait, mais oùvolatile
ne serait pas. L' utilisation volatile pour cela serait UB, même si cela fonctionne en pratique sur de vraies implémentations C ++.)Pour éviter que cela se produise, le compilateur doit être explicitement averti de ne pas le faire.
Je suis un peu surpris par l'évolution de la discussion concernant la relation potentielle de
volatile
cette question. Ainsi, je voudrais dépenser mes deux cents:la source
func()
et j'ai pensé "Je pourrais optimiser cela" L'optimiseur ne se soucie pas du tout des threads, et détectera la boucle infinie, et la transformera avec plaisir en un "moment (Vrai)" Si nous regardons Godbolt .org / z / Tl44iN nous pouvons le voir. Si c'est fini,True
il revient. Sinon, il entre dans un saut inconditionnel de retour à lui-même (une boucle infinie) au label.L5
volatile
en C ++ 11 car vous pouvez obtenir un asm identique avecatomic<T>
etstd::memory_order_relaxed
. Cela fonctionne bien sur du matériel réel: les caches sont cohérents, donc une instruction de chargement ne peut pas continuer à lire une valeur périmée une fois qu'un magasin sur un autre noyau s'engage à y mettre en cache. (MESI)volatile
est toujours UB. Vous ne devriez vraiment jamais supposer que quelque chose est définitivement et clairement UB est sûr juste parce que vous ne pouvez pas penser à une façon dont cela pourrait mal tourner et cela a fonctionné lorsque vous l'avez essayé. Cela a incité les gens à brûler encore et encore.finished
avec unstd::mutex
works (sansvolatile
ouatomic
). En fait, vous pouvez remplacer tous les atomes par un schéma "simple" valeur + mutex; cela fonctionnerait toujours et serait plus lent.atomic<T>
est autorisé à utiliser un mutex interne; seulatomic_flag
est garanti sans verrou.La réponse de Scheff décrit comment réparer votre code. J'ai pensé ajouter quelques informations sur ce qui se passe réellement dans ce cas.
J'ai compilé votre code sur godbolt en utilisant le niveau d'optimisation 1 (
-O1
). Votre fonction se compile comme suit:Alors, que se passe-t-il ici? Tout d'abord, nous avons une comparaison:
cmp BYTE PTR finished[rip], 0
- cela vérifie sifinished
c'est faux ou non.Si ce n'est pas faux (ou vrai), nous devons quitter la boucle lors de la première exécution. Ceci accompli par
jne .L4
lequel j umps quand n ot e qual pour étiqueter.L4
où la valeur dei
(0
) est stockée dans un registre pour une utilisation ultérieure et la fonction retourne.S'il est faux cependant, nous passons à
Il s'agit d'un saut inconditionnel, à étiqueter
.L5
qui se trouve être la commande de saut elle-même.En d'autres termes, le thread est placé dans une boucle occupée infinie.
Alors pourquoi est-ce arrivé?
En ce qui concerne l'optimiseur, les threads sont en dehors de sa compétence. Il suppose que les autres threads ne lisent ni n'écrivent les variables simultanément (car ce serait l'UB de la course aux données). Vous devez lui dire qu'il ne peut pas optimiser les accès. C'est là que la réponse de Scheff entre en jeu. Je ne prendrai pas la peine de le répéter.
Étant donné que l'optimiseur n'est pas informé que la
finished
variable peut potentiellement changer pendant l'exécution de la fonction, il voit qu'ilfinished
n'est pas modifié par la fonction elle-même et suppose qu'elle est constante.Le code optimisé fournit les deux chemins de code qui résulteront de l'entrée de la fonction avec une valeur booléenne constante; soit il exécute la boucle à l'infini, soit la boucle n'est jamais exécutée.
au
-O0
compilateur (comme prévu) n'optimise pas le corps de la boucle et la comparaison:par conséquent, la fonction, lorsqu'elle n'est pas optimisée, fonctionne, le manque d'atomicité ici n'est généralement pas un problème, car le code et le type de données sont simples. Le pire que nous pourrions rencontrer ici est probablement une valeur
i
qui est inférieure de un à ce qu'elle devrait être.Un système plus complexe avec des structures de données est beaucoup plus susceptible d'entraîner des données corrompues ou une exécution incorrecte.
la source
atomic
variables dans du code qui n'écrit pas ces variables. Par exemple,if (cond) foo=1;
ne peut pas être transformé en asm, c'est commefoo = cond ? 1 : foo;
parce que load + store (pas un RMW atomique) pourrait marcher sur une écriture d'un autre thread. Les compilateurs évitaient déjà des trucs comme ça parce qu'ils voulaient être utiles pour écrire des programmes multi-threads, mais C ++ 11 a rendu officiel que les compilateurs ne devaient pas casser le code là où 2 threads écriventa[1]
eta[2]
Par souci d'exhaustivité dans la courbe d'apprentissage; vous devez éviter d'utiliser des variables globales. Vous avez cependant fait du bon travail en le rendant statique, il sera donc local à l'unité de traduction.
Voici un exemple:
En direct sur wandbox
la source
finished
commestatic
dans le bloc fonction. Il sera toujours initialisé une seule fois, et s'il est initialisé à une constante, cela ne nécessite pas de verrouillage.finished
pourraient également utiliser desstd::memory_order_relaxed
charges et des magasins moins chers ; il n'y a pas de commande requise. d'autres variables dans l'un ou l'autre thread. Je ne suis pas sûr que la suggestion de @ Davislor ait dustatic
sens, cependant; si vous aviez plusieurs threads de spin-count, vous ne voudriez pas nécessairement les arrêter tous avec le même drapeau. Vous voulez écrire l'initialisation d'finished
une manière qui se compile en une simple initialisation, pas un magasin atomique, cependant. (Comme vous le faites avec lafinished = false;
syntaxe C ++ 17 de l'initialiseur par défaut. Godbolt.org/z/EjoKgq ).