Les types fondamentaux C / C ++ sont-ils atomiques?

Les types fondamentaux C / C ++, tels que int , double , etc., sont-ils atomiques, par exemple threadsafe?

Sont-ils libres de courses de données? en d’autres termes, si un thread écrit sur un object de ce type alors qu’un autre thread en lit un, le comportement est-il bien défini?

Sinon, cela dépend-il du compilateur ou de quelque chose d’autre?

Non, les types de données fondamentaux (par exemple, int , double ) ne sont pas atomiques, voir std::atomic .

Au lieu de cela, vous pouvez utiliser std::atomic ou std::atomic .

Note: std::atomic été introduit avec C ++ 11 et je pense qu’avant C ++ 11, le standard C ++ ne reconnaissait pas l’existence du multithreading.


Comme le souligne @Josh, std::atomic_flag est un type booléen atomique. Il est garanti sans verrou , contrairement aux spécialisations std::atomic .


La documentation citée est tirée de: http://open-std.org/JTC1/SC22/WG21/docs/papers/2015/n4567.pdf . Je suis sûr que la norme n’est pas gratuite et que ce n’est pas la version finale / officielle.

1.10 Exécutions multi-threads et courses de données

  1. 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.
  2. La bibliothèque définit un nombre d’opérations atomiques (Clause 29) et d’opérations sur les mutex (Clause 30) qui sont spécialement identifiées comme des opérations de synchronisation. Ces opérations jouent un rôle particulier dans la réalisation d’affectations dans un fil visible par un autre. Une opération de synchronisation sur un ou plusieurs emplacements de mémoire est une opération de consommation, une opération d’acquisition, une opération de libération ou 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 une clôture d’acquisition, une clôture de libération ou une clôture d’acquisition et de libération. En outre, il existe des opérations atomiques assouplies, qui ne sont pas des opérations de synchronisation, et des opérations de lecture-modification-écriture atomiques, qui ont des caractéristiques spéciales.

  1. Deux actions sont potentiellement concurrentes si
    (23.1) – ils sont exécutés par des threads différents, ou
    (23.2) – ils ne sont pas séquencés et au moins un est exécuté par un gestionnaire de signaux.
    L’exécution d’un programme contient une course de données si elle contient deux actions conflictuelles potentiellement concurrentes, dont l’une n’est pas atomique, et aucune ne se produit avant l’autre, à l’exception du cas particulier des gestionnaires de signaux décrit ci-dessous. Toute course de données de ce type entraîne un comportement indéfini.

29.5 Types atomiques

  1. Il y aura des spécialisations explicites du gabarit atomique pour les types intégraux “char, char signed char , unsigned char , short , unsigned short , int , unsigned int , long , unsigned long , long long , unsigned long long , char16_ , char32_t , wchar_t , et tout autre type requirejs par les typedefs dans l’en-tête . Pour chaque intégrale de type intégral, l’intégrale atomic spécialisation fournit des opérations atomiques supplémentaires adaptées aux types intégraux. Il doit y avoir une spécialisation atomic qui fournit les opérations atomiques générales spécifiées au 29.6.1.

  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 sortingviaux par défaut et des destructeurs sortingviaux. Ils doivent chacun prendre en charge la syntaxe d’initialisation globale.

29.7 Type de drapeau et opérations

  1. Les opérations sur un object de type atomic_flag doivent être exemptes de locking. [Note: les opérations doivent donc être exemptes d’adresses. Aucun autre type ne nécessite d’opérations sans locking, de sorte que le type atomic_flag est le type implémenté par matériel minimum requirejs pour se conformer à cette norme internationale. Les autres types peuvent être émulés avec atomic_flag, mais avec des propriétés moins qu’idéales. – note finale]

Comme C est aussi (actuellement) mentionné dans la question même s’il ne figure pas dans les balises, le standard C stipule:

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 objects qui ne sont ni des objects atomiques sans locking ni du type volatile sig_atomic_t sont pas spécifiées, de même que l’état de l’environnement à virgule flottante. La valeur de tout object modifié par le gestionnaire qui n’est ni un object atomique sans locking ni de type volatile sig_atomic_t devient indéterminée lorsque le gestionnaire quitte, de même que l’état de l’environnement en virgule flottante s’il est modifié par le gestionnaire et non restauré à son état d’origine.

et

5.1.2.4 Exécutions multi-threads et courses de données

Deux évaluations d’expression sont en conflit si l’une d’entre elles modifie un emplacement de mémoire et que l’autre lit ou modifie le même emplacement de mémoire.

[plusieurs pages de normes – quelques paragraphes traitant explicitement des types atomiques]

L’exécution d’un programme contient une course de données si elle contient deux actions en conflit dans des threads différents, dont au moins une n’est pas atomique et aucune ne se produit avant l’autre. Toute course de données de ce type entraîne un comportement indéfini.

Notez que les valeurs sont “indéterminées” si un signal interrompt le traitement et que l’access simultané aux types non explicitement atomiques est un comportement indéfini.

Qu’est ce que l’atome?

Atomic, comme décrivant quelque chose avec la propriété d’un atome. Le mot atome provient du latin atomus qui signifie “indivis”.

En général, je pense à une opération atomique (indépendamment de la langue) pour avoir deux qualités:

Une opération atomique est toujours indivise.

C’est-à-dire que cela se fait de manière indivisible, je pense que c’est ce que OP appelle “threadsafe”. Dans un sens, l’opération se déroule instantanément lorsqu’elle est visualisée par un autre thread.

Par exemple, l’opération suivante est probablement divisée (dépend du compilateur / du matériel):

 i += 1; 

car il peut être observé par un autre thread (sur le matériel hypothétique et le compilateur) comme:

 load r1, i; addi r1, #1; store i, r1; 

Deux threads effectuant l’opération ci-dessus i += 1 sans synchronisation appropriée peuvent produire un résultat incorrect. Disons i=0 initialement, le thread T1 charge T1.r1 = 0 , et le thread T2 charge t2.r1 = 0 . Les deux threads incrémentent leurs r1 respectives de 1, puis stockent le résultat dans i . Bien que deux incréments aient été effectués, la valeur de i est toujours de 1 car l’opération d’incrément était divisible. Notez que s’il y avait eu une synchronisation avant et après i+=1 l’autre thread aurait attendu que l’opération soit terminée et aurait donc observé une opération non divisée.

Notez que même une simple écriture peut être ou non indivise:

 i = 3; store i, #3; 

en fonction du compilateur et du matériel. Par exemple, si l’adresse de i n’est pas correctement alignée, il faut utiliser un chargement / magasin non aligné exécuté par le CPU sous la forme de plusieurs charges / magasins plus petits.

Une opération atomique a garanti la sémantique de l’ordre de la mémoire.

Les opérations non atomiques peuvent être réorganisées et peuvent ne pas nécessairement se produire dans l’ordre écrit dans le code source du programme.

Par exemple, sous la règle “as-if”, le compilateur est autorisé à réorganiser les magasins et les charges comme il l’entend tant que tout access à la mémoire volatile se produit dans l’ordre spécifié par le programme “comme si le programme était évalué selon le libellé de la norme. Ainsi, les opérations non atomiques peuvent être ré-agencées en cassant toute hypothèse concernant l’ordre d’exécution dans un programme multithread. C’est pourquoi une utilisation apparemment innocente d’un int brut en tant que variable de signalisation dans une programmation multithread est rompue, même si les écritures et les lectures peuvent être indivisibles, le classement peut rompre le programme en fonction du compilateur. Une opération atomique impose l’organisation des opérations autour de celle-ci en fonction de la sémantique de mémoire spécifiée. Voir std::memory_order .

Le processeur peut également réorganiser vos access mémoire en fonction des contraintes de commande de la mémoire de ce processeur. Vous pouvez trouver les contraintes de classement de la mémoire pour l’architecture x86 dans la section 8.2 du Manuel du développeur des architectures Intel 64 et IA32 à partir de la page 2212.

Les types primitifs ( int , char etc.) ne sont pas atomiques

Parce que même sous certaines conditions, ils peuvent avoir des instructions de stockage et de chargement indivisibles ou même des instructions arithmétiques, ils ne garantissent pas le classement des magasins et des charges. En tant que tels, ils sont dangereux à utiliser dans des contextes multithread sans synchronisation appropriée pour garantir que l’état de mémoire observé par les autres threads est ce que vous pensez qu’il est à ce moment-là.

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 vous utilisez std::atomic , par exemple, et que bool est réellement atomique sur l’architecture cible, le compilateur ne générera pas de clôtures ni de verrous redondants. Le même code serait généré comme pour un bool ordinaire.

En d’autres termes, l’utilisation de std::atomic ne fait que rendre le code moins efficace s’il est requirejs de l’exactitude sur la plate-forme. Il n’y a donc aucune raison de l’éviter.