web-dev-qa-db-fra.com

Qu'est-ce que std :: atomic?

Je comprends que std::atomic<> est un objet atomique. Mais atomique dans quelle mesure? À ma connaissance, une opération peut être atomique. Qu'entend-on exactement par rendre un objet atomique? Par exemple, si deux threads exécutent simultanément le code suivant:

a = a + 12;

Alors l’opération entière (disons add_twelve_to(int)) est-elle atomique? Ou bien des modifications sont-elles apportées à la variable atomique (donc operator=())?

113
user4386938

Chaque instanciation et spécialisation complète de std :: atomic <> représente un type sur lequel différents threads peuvent agir simultanément (leurs instances), sans générer de comportement indéfini:

Les objets de types atomiques sont les seuls objets C++ libres de courses de données; En d'autres termes, si un thread écrit dans un objet atomique pendant qu'un autre thread en lit, le comportement est bien défini.

De plus, les accès aux objets atomiques peuvent établir une synchronisation inter-thread et ordonner des accès à la mémoire non atomique comme spécifié par _std::memory_order_.

_std::atomic<>_ encapsule les opérations qui, 11 fois avant C++, devaient être effectuées avec (par exemple) fonctions inter-verrouillées avec MSVC ou bultins atomiques dans le cas de GCC .

De plus, _std::atomic<>_ vous donne plus de contrôle en autorisant divers ordres de mémoire qui spécifient des contraintes de synchronisation et d’ordre. Si vous souhaitez en savoir plus sur C++ 11 atomics et le modèle de mémoire, ces liens peuvent être utiles:

Notez que, dans les cas d'utilisation typiques, vous utiliserez probablement opérateurs arithmétiques surchargés ou n autre jeu d'entre eux :

_std::atomic<long> value(0);
value++; //This is an atomic op
value += 5; //And so is this
_

Comme la syntaxe de l'opérateur ne vous permet pas de spécifier l'ordre de la mémoire, ces opérations seront effectuées avec std::memory_order_seq_cst , car il s'agit de l'ordre par défaut pour toutes les opérations atomiques dans C++ 11. Il garantit la cohérence séquentielle. (classement global total) entre toutes les opérations atomiques.

Dans certains cas, cependant, cela peut ne pas être nécessaire (et rien ne vient gratuitement), vous pouvez donc utiliser une forme plus explicite:

_std::atomic<long> value {0};
value.fetch_add(1, std::memory_order_relaxed); // Atomic, but there are no synchronization or ordering constraints
value.fetch_add(5, std::memory_order_release); // Atomic, performs 'release' operation
_

Maintenant, votre exemple:

_a = a + 12;
_

ne sera pas évalué à une opération atomique unique: il en résultera a.load() (qui est atomique lui-même), puis addition entre cette valeur et _12_ et a.store() (également atomique) du résultat final. Comme je l'ai indiqué précédemment, _std::memory_order_seq_cst_ sera utilisé ici.

Cependant, si vous écrivez _a += 12_, ce sera une opération atomique (comme je l’ai déjà noté) et équivaut approximativement à a.fetch_add(12, std::memory_order_seq_cst).

Quant à ton commentaire:

Un int régulier a des charges et des mémoires atomiques. Quel est l'intérêt de l'envelopper avec _atomic<>_?

Votre déclaration n'est valable que pour les architectures offrant une telle garantie d'atomicité pour les magasins et/ou les charges. Il y a des architectures qui ne le font pas. En outre, il est généralement nécessaire que les opérations effectuées sur une adresse alignée Word/dword pour être atomique _std::atomic<>_ est quelque chose qui est garanti pour être atomique sur chaque plate-forme , sans exigences supplémentaires. De plus, cela vous permet d'écrire du code comme ceci:

_void* sharedData = nullptr;
std::atomic<int> ready_flag = 0;

// Thread 1
void produce()
{
    sharedData = generateData();
    ready_flag.store(1, std::memory_order_release);
}

// Thread 2
void consume()
{
    while (ready_flag.load(std::memory_order_acquire) == 0)
    {
        std::this_thread::yield();
    }

    assert(sharedData != nullptr); // will never trigger
    processData(sharedData);
}
_

Notez que la condition d'assertion sera toujours vraie (et donc, ne déclenchera jamais), ainsi vous pouvez toujours être sûr que les données sont prêtes après la sortie de la boucle while. C'est parce que:

  • store() pour que l'indicateur soit activé après que sharedData soit défini (nous supposons que generateData() renvoie toujours quelque chose d'utile, en particulier, ne renvoie jamais NULL) et utilise _std::memory_order_release_ ordre:

_memory_order_release_

Une opération de stockage avec cet ordre de mémoire effectue la libération : aucune lecture ni écriture dans le fil actuel ne peuvent être réorganisées . après ce magasin. Toutes les écritures dans le thread actuel sont visibles dans les autres threads qui acquièrent la même variable atomique

  • sharedData est utilisé après la sortie de la boucle while, et donc après load() de fan retournera une valeur non nulle. load() utilise _std::memory_order_acquire_ order:

_std::memory_order_acquire_

Une opération de chargement avec cet ordre de mémoire effectue l’opération d’acquisition sur l’emplacement de mémoire affecté: aucune lecture ni écriture dans le thread en cours ne peuvent être réorganisées avant cette charge. Toutes les écritures dans d'autres threads qui libèrent la même variable atomique sont visibles dans le thread actuel .

Cela vous donne un contrôle précis sur la synchronisation et vous permet de spécifier explicitement comment votre code peut/peut ne pas/va/ne se comportera pas. Cela ne serait pas possible si la seule garantie était l'atomicité elle-même. Surtout quand il s’agit de modèles de synchronisation très intéressants comme le commande de libération-consommation .

122
Mateusz Grzejek

Je comprends que std::atomic<> rend un objet atomique.

C'est une question de perspective ... vous ne pouvez pas l'appliquer à des objets arbitraires et leurs opérations deviennent atomiques, mais les spécialisations fournies pour (la plupart) les types et pointeurs intégraux peuvent être utilisées.

a = a + 12;

std::atomic<> ne simplifie pas cette opération en une seule opération atomique, mais le membre operator T() const volatile noexcept effectue un atomique load() sur a, puis le nombre douze est ajouté. operator=(T t) noexcept effectue une store(t).

17
Tony Delroy