Sont des types fondamentaux C/C++, comme int
, double
, etc., atomiques, par ex. threadsafe?
Sont-ils exempts de courses aux données; c'est-à-dire que si un thread écrit sur un objet d'un tel type alors qu'un autre thread le lit, le comportement est-il bien défini?
Sinon, cela dépend-il du compilateur ou d'autre chose?
Non, les types de données fondamentaux (par exemple, int
, double
) ne sont pas atomiques, voir std::atomic
.
À la place, vous pouvez utiliser std::atomic<int>
ou std::atomic<double>
.
Remarque: std::atomic
a été introduit avec C++ 11 et je crois comprendre qu'avant C++ 11, la norme C++ ne reconnaissait pas du tout l'existence du multithreading.
Comme l'a souligné @Josh, std::atomic_flag
est un type booléen atomique. Il est garanti sans verrouillage , contrairement au std::atomic
spécialisations.
La documentation citée provient de: http://open-std.org/JTC1/SC22/WG21/docs/papers/2015/n4567.pdf . Je suis presque sûr que le standard n'est pas gratuit et donc ce n'est pas la version finale/officielle.
- Deux évaluations d'expression sont en conflit si l'une d'entre elles modifie un emplacement mémoire (1.7) et que l'autre lit ou modifie le même emplacement mémoire.
- La bibliothèque définit un certain nombre d'opérations atomiques (article 29) et d'opérations sur les mutex (article 30) qui sont spécialement identifiées comme des opérations de synchronisation. Ces opérations jouent un rôle spécial en rendant les affectations dans un thread visibles pour un autre. Une opération de synchronisation sur un ou plusieurs emplacements de mémoire est soit une opération de consommation, une opération d'acquisition, une opération de libération, soit les deux une opération d'acquisition et de libération. Une opération de synchronisation sans emplacement de mémoire associé est une clôture et peut être soit une clôture d'acquisition, une clôture de libération, soit les deux une clôture d'acquisition et de libération. De plus, il existe des opérations atomiques détendues, qui ne sont pas des opérations de synchronisation, et des opérations atomiques de lecture-modification-écriture, qui ont des caractéristiques spéciales.
- Deux actions sont potentiellement simultanées si
(23.1) - ils sont exécutés par différents threads, ou
(23.2) - elles ne sont pas séquencées, et au moins une est exécutée par un gestionnaire de signaux.
. Une telle course de données entraîne un comportement indéfini.
- Il doit y avoir des spécialisations explicites du modèle atomique pour les types intégraux `` char,
signed char
,unsigned char
,short
,unsigned short
,int
,unsigned int
,long
,unsigned long
,long long
,unsigned long long
,char16_
t,char32_t
,wchar_t
, et tout autre type requis par les typedefs dans l'en-tête<cstdint>
. Pour chaque intégrale de type intégrale, la spécialisationatomic<integral>
fournit des opérations atomiques supplémentaires appropriées aux types intégraux. Il y aura une spécialisationatomic<bool>
qui fournit les opérations atomiques générales spécifiées au 29.6.1 ..
- Il doit y avoir des spécialisations partielles de pointeur du modèle de classe atomique. Ces spécialisations doivent avoir une disposition standard, des constructeurs par défaut triviaux et des destructeurs triviaux. Ils doivent chacun prendre en charge la syntaxe d'initialisation agrégée.
- Les opérations sur un objet de type atomic_flag doivent être sans verrouillage. [Remarque: Par conséquent, les opérations doivent également être sans adresse. Aucun autre type ne nécessite d'opérations sans verrouillage, donc le type atomic_flag est le type minimal implémenté par le matériel nécessaire pour se conformer à cette norme internationale. Les types restants peuvent être émulés avec atomic_flag, mais avec des propriétés moins qu'idéales. - note de fin]
Étant donné que C est également (actuellement) mentionné dans la question bien qu'il ne soit pas dans les balises, le C Standard indique:
5.1.2.3 Exécution du programme
...
Lorsque le traitement de la machine abstraite est interrompu par la réception d'un signal, les valeurs des objets qui ne sont ni des objets atomiques sans verrouillage ni de type
volatile sig_atomic_t
ne sont pas spécifiés, de même que l'état de l'environnement à virgule flottante. La valeur de tout objet modifié par le gestionnaire qui n'est ni un objet atomique sans verrou ni de typevolatile sig_atomic_t
devient indéterminé lorsque le gestionnaire se termine, tout comme l'état de l'environnement à virgule flottante s'il est modifié par le gestionnaire et n'est pas restauré à son état d'origine.
et
5.1.2.4 Exécutions multithread et courses de données
...
Deux évaluations d'expressions conflit si l'une d'elles modifie un emplacement mémoire et l'autre lit ou modifie le même emplacement mémoire.
[plusieurs pages de normes - quelques paragraphes traitant explicitement des types atomiques]
L'exécution d'un programme contient une course aux données s'il contient deux actions conflictuelles dans des threads différents, dont au moins une n'est pas atomique, et aucune ne se produit avant l'autre. Une telle course de données entraîne un comportement indéfini.
Notez que les valeurs sont "indéterminées" si un signal interrompt le traitement, et l'accès simultané à des types qui ne sont pas explicitement atomiques est un comportement non défini.
Atomique, comme décrivant quelque chose avec la propriété d'un atome. Le mot atom provient du latin atomus signifiant "indivis".
En général, je pense qu'une opération atomique (quelle que soit la langue) a deux qualités:
C'est à dire. il est effectué de manière indivisible, je crois que c'est ce que OP appelle "threadsafe". Dans un sens, l'opération se produit instantanément lorsqu'elle est vue par un autre thread.
Par exemple, l'opération suivante est probablement divisée (en fonction du compilateur/matériel):
i += 1;
car il peut être observé par un autre thread (sur un matériel et un compilateur hypothétique) comme:
load r1, i;
addi r1, #1;
store i, r1;
Deux threads effectuant l'opération ci-dessus i += 1
sans synchronisation appropriée peut produire un résultat incorrect. Dire i=0
initialement, thread T1
charges T1.r1 = 0
, et le fil T2
charges t2.r1 = 0
. Les deux threads incrémentent leur r1
s par 1, puis stockez le résultat dans i
. Bien que deux incréments aient été effectués, la valeur de i
n'est toujours que de 1 car l'opération d'incrémentation était divisible. Notez qu'il y avait eu une synchronisation avant et après i+=1
l'autre thread aurait attendu la fin de l'opération et aurait donc observé une opération indivise.
Notez que même une simple écriture peut être indivise ou non:
i = 3;
store i, #3;
selon le compilateur et le matériel. Par exemple, si l'adresse de i
n'est pas alignée convenablement, alors un chargement/magasin non aligné doit être utilisé qui est exécuté par le CPU comme plusieurs petits chargements/magasins.
Les opérations non atomiques peuvent être réordonnées et ne peuvent pas nécessairement se produire dans l'ordre écrit dans le code source du programme.
Par exemple, selon la règle "as-if" le compilateur est autorisé à réorganiser les magasins et les chargements comme bon lui semble tant que tous les accès à la mémoire volatile se produisent dans l'ordre spécifié par le programme "comme si" le programme a été évalué selon le libellé de la norme. Ainsi, les opérations non atomiques peuvent être réorganisées en rompant toutes les hypothèses sur l'ordre d'exécution dans un programme multithread. C'est pourquoi une utilisation apparemment innocente d'un int
brut comme variable de signalisation dans la programmation multi-thread est interrompue, même si les écritures et les lectures peuvent être indivisibles, l'ordre peut interrompre le programme en fonction du compilateur. Une opération atomique applique l'ordre des opérations autour d'elle en fonction de la mémoire sémantique spécifiée. Voir std::memory_order
.
Le processeur peut également réorganiser vos accès à la mémoire sous les contraintes de commande de mémoire de ce processeur. Vous pouvez trouver les contraintes d'ordre de mémoire pour l'architecture x86 dans la section Intel 64 et IA32 Architectures Software Developer Manual à partir de la page 2212.
int
, char
etc) ne sont pas atomiquesParce que même si, dans certaines conditions, ils peuvent avoir des instructions de stockage et de chargement indivisibles ou peut-être même des instructions arithmétiques, ils ne garantissent pas la commande des magasins et des charges. En tant que tels, ils ne sont pas sûrs à utiliser dans des contextes multithreads sans synchronisation appropriée pour garantir que l'état de la mémoire observé par d'autres threads est ce que vous pensez qu'il est à ce moment.
J'espère que cela explique pourquoi les types primitifs ne sont pas atomiques.
Une information supplémentaire que je n'ai pas vue mentionnée dans les autres réponses jusqu'à présent:
Si tu utilises std::atomic<bool>
, par exemple, et bool
est en fait atomique sur l'architecture cible, alors le compilateur ne générera pas de barrières ou de verrous redondants. Le même code serait généré que pour un bool
simple.
En d'autres termes, en utilisant std::atomic
ne rend le code moins efficace que s'il est réellement requis pour être correct sur la plate-forme. Il n'y a donc aucune raison de l'éviter.