Guide de concurrence C ++ std :: atomic

Introduction de base à std :: atomic

std :: atomic est une classe de modèle. Un objet atomique de type de modèle T encapsule une valeur de type T.

template struct atomic;

La principale caractéristique des objets de type atomique est que l'accès à partir de différents threads ne provoque pas de courses de données. Par conséquent, accéder à un objet atomique à partir de différents threads est un comportement bien défini, et généralement pour les types non atomiques, l'accès simultané à un objet (si aucune opération de synchronisation n'est effectuée) entraînera un comportement indéfini.

std :: constructeur atomique

Le constructeur de std :: atomic est le suivant:
Insérez la description de l'image ici

  1. Le constructeur par défaut, l'objet std :: atomic créé par le constructeur par défaut est dans l'état non initialisé, et l'objet std :: atomic dans l'état non initialisé peut être initialisé par la fonction atomic_init.
  2. Initialisez le constructeur, initialisez un objet std :: atomic de type T.
  3. Le constructeur de copie est désactivé.

Prenons l'exemple suivant:

#include <iostream>       // std::cout
#include <atomic>         // std::atomic, std::atomic_flag, ATOMIC_FLAG_INIT
#include <thread>         // std::thread, std::this_thread::yield
#include <vector>         // std::vector
 
// 由 false 初始化一个 std::atomic<bool> 类型的原子变量
std::atomic<bool> ready(false);
std::atomic_flag winner = ATOMIC_FLAG_INIT;
 
void do_count1m(int id)
{
    
    
    while (!ready) {
    
     
        std::this_thread::yield();  // 等待 ready 变为 true.
    }
    
    for (volatile int i=0; i<1000000; ++i) {
    
    } // 计数
 
    if (!winner.test_and_set()) {
    
    
      std::cout << "thread #" << id << " won!\n";
    }
}
 
int main ()
{
    
    
    std::vector<std::thread> threads;
    std::cout << "spawning 10 threads that count to 1 million...\n";
    
    for (int i=1; i<=10; ++i) 
        threads.push_back(std::thread(do_count1m,i));
        
    ready = true;
 
    for (auto& th : threads) 
        th.join();
        
    return 0;
}

fonction std :: atomic :: operator = ()

La fonction d'opération d'affectation de std :: atomic est définie comme suit: On
Insérez la description de l'image ici
peut voir que l'opération de copie d'affectation ordinaire a été désactivée. Mais une variable de type T peut être affectée à la variable de type atomique correspondante (équivalente à une conversion implicite), l'opération est atomique et l'ordre de la mémoire (Ordre de la mémoire) prend par défaut la cohérence séquentielle (std :: memory_order_seq_cst), si vous avez besoin de spécifier Pour les autres séquences de mémoire, utilisez std :: atomic :: store ().

#include <iostream>             // std::cout
#include <atomic>               // std::atomic
#include <thread>               // std::thread, std::this_thread::yield
 
std::atomic<int> foo(0);
 
void set_foo(int x)
{
    
    
    foo = x; // 调用 std::atomic::operator=().
}
 
void print_foo()
{
    
    
    while (foo == 0) {
    
     // wait while foo == 0
        std::this_thread::yield();
    }
    std::cout << "foo: " << foo << '\n';
}
 
int main()
{
    
    
    std::thread first(print_foo);
    std::thread second(set_foo, 10);
    
    first.join();
    second.join();
    
    return 0;
}

Opérations de base de type std :: atomic

is_lock_free

bool is_lock_free() const volatile noexcept;
bool is_lock_free() const noexcept;

Déterminez si l'objet std :: atomic a des caractéristiques sans verrouillage. Si un objet satisfait la fonctionnalité sans verrouillage, il ne provoquera pas de blocage de thread lorsque plusieurs threads accèdent à l'objet. (Peut utiliser une sorte de méthode de mémoire transactionnelle pour obtenir des fonctionnalités sans verrouillage).

boutique

void store (T val, memory_order sync = memory_order_seq_cst) volatile noexcept;
void store (T val, memory_order sync = memory_order_seq_cst) noexcept;

Pour modifier la valeur encapsulée, la fonction std :: atomic :: store copie le paramètre val de type T dans la valeur encapsulée par l'objet atomique. T est le paramètre de modèle de classe std :: atomic. En outre, le paramètre sync spécifie l'ordre de la mémoire et les valeurs possibles sont les suivantes:
Insérez la description de l'image ici
Veuillez consulter l'exemple suivant:

#include <iostream>       // std::cout
#include <atomic>         // std::atomic, std::memory_order_relaxed
#include <thread>         // std::thread
 
std::atomic<int> foo(0);  // 全局的原子对象 foo
 
void set_foo(int x)
{
    
    
    foo.store(x, std::memory_order_relaxed);     // 设置(store) 原子对象 foo 的值
}
 
void print_foo()
{
    
    
    int x;
    do {
    
    
        x = foo.load(std::memory_order_relaxed); // 读取(load) 原子对象 foo 的值
    } while (x == 0);
    
    std::cout << "foo: " << x << '\n';
}
 
int main ()
{
    
    
    std::thread first(print_foo);     // 线程 first 打印 foo 的值
    std::thread second(set_foo, 10);  // 线程 second 设置 foo 的值
    
    first.join();
    second.join();
    
    return 0;
}

charger

T load (memory_order sync = memory_order_seq_cst) const volatile noexcept;
T load (memory_order sync = memory_order_seq_cst) const noexcept;

Lisez la valeur encapsulée, la synchronisation des paramètres définit l'ordre de la mémoire (ordre de la mémoire), les valeurs possibles sont les suivantes:
Insérez la description de l'image ici
veuillez consulter l'exemple de stockage ci-dessus.

opérateur T

operator T() const volatile noexcept;
operator T() const noexcept;

Semblable à la fonction load, elle lit également la valeur encapsulée. L'opérateur T () est une opération de conversion de type. L'ordre de mémoire par défaut est std :: memory_order_seq_cst. Si vous devez spécifier d'autres commandes de mémoire, vous devez utiliser la fonction load () .
Prenons l'exemple suivant:

#include <iostream>       // std::cout
#include <atomic>         // std::atomic
#include <thread>         // std::thread, std::this_thread::yield
 
std::atomic<int> foo(0);
std::atomic<int> bar(0);
 
void set_foo(int x)
{
    
    
    foo = x;
}
 
void copy_foo_to_bar()
{
    
    
 
    // 如果 foo == 0,则该线程 yield,
    // 在 foo == 0 时, 实际也是隐含了类型转换操作,
    // 因此也包含了 operator T() const 的调用.
    while (foo == 0) 
        std::this_thread::yield();
 
    // 实际调用了 operator T() const, 将foo 强制转换成 int 类型,
    // 然后调用 operator=().
    bar = static_cast<int>(foo);
}
 
void print_bar()
{
    
    
    // 如果 bar == 0,则该线程 yield,
    // 在 bar == 0 时, 实际也是隐含了类型转换操作,
    // 因此也包含了 operator T() const 的调用.
    while (bar == 0) 
        std::this_thread::yield();
    std::cout << "bar: " << bar << '\n';
}
 
int main ()
{
    
    
    std::thread first(print_bar);
    std::thread second(set_foo, 10);
    std::thread third(copy_foo_to_bar);
 
    first.join();
    second.join();
    third.join();
    return 0;
}

échanger

T exchange (T val, memory_order sync = memory_order_seq_cst) volatile noexcept;
T exchange (T val, memory_order sync = memory_order_seq_cst) noexcept;

Lisez et modifiez la valeur encapsulée, l'échange remplacera la valeur spécifiée par val par la valeur encapsulée par l'objet atomique avant, et retournera la valeur encapsulée par l'objet atomique avant. L'ensemble du processus est atomique (l'opération d'échange est donc également appelée lecture - opération de modification-écriture).
Le paramètre sync spécifie l'ordre de la mémoire (ordre de la mémoire) et les valeurs possibles sont les suivantes:
Insérez la description de l'image ici
Veuillez consulter l'exemple ci-dessous, chaque thread compte jusqu'à 1 Mo et le thread qui termine la tâche de comptage imprime d'abord son propre ID.

#include <iostream>       // std::cout
#include <atomic>         // std::atomic
#include <thread>         // std::thread
#include <vector>         // std::vector
 
std::atomic<bool> ready(false);
std::atomic<bool> winner(false);
 
void count1m (int id)
{
    
    
    while (!ready) {
    
    }                      // wait for the ready signal
    for (int i = 0; i < 1000000; ++i) {
    
    }   // go!, count to 1 million
    
    if (!winner.exchange(true)) {
    
     
        std::cout << "thread #" << id << " won!\n"; 
    }
};
 
int main ()
{
    
    
    std::vector<std::thread> threads;
    std::cout << "spawning 10 threads that count to 1 million...\n";
    
    for (int i = 1; i <= 10; ++i) 
        threads.push_back(std::thread(count1m,i));
    
    ready = true;
    
    for (auto& th : threads) 
        th.join();
 
    return 0;
}

compare_exchange_weak

Insérez la description de l'image ici
Comparez et échangez si la valeur encapsulée (faible) est égale à la valeur spécifiée par le paramètre attendu, si:

  • S'ils sont égaux, remplacez l'ancienne valeur de l'objet atomique par val.
  • S'il n'est pas égal, remplacez attendu par l'ancienne valeur de l'objet atomique. Par conséquent, après avoir appelé cette fonction, si la valeur encapsulée par l'objet atomique n'est pas égale à la valeur spécifiée par le paramètre attendu, le contenu attendu est le ancienne valeur de l'objet atomique.

Cette fonction lit généralement la valeur encapsulée par l'objet atomique. Si la comparaison est vraie (c'est-à-dire que la valeur de l'objet atomique est égale à attendue), l'ancienne valeur de l'objet atomique est remplacée, mais l'opération entière est atomique. Lire et modifier la valeur dans un thread Lorsqu'un objet atomique est utilisé, un autre thread ne peut pas lire ou modifier l'objet atomique.

Dans le cas (2), le choix de l’ordre de mémoire dépend du résultat de l’opération de comparaison. Si le résultat de la comparaison est vrai (c’est-à-dire que la valeur de l’objet atomique est égale à celle attendue), l’ordre de mémoire spécifié par le paramètre succès est sélectionné, sinon le paramètre est sélectionné échec La séquence de mémoire spécifiée.

Notez que cette fonction compare directement la valeur encapsulée par l'objet atomique avec le contenu physique du paramètre attendu. Par conséquent, dans certains cas, l'opération de comparaison de l'objet est égale lorsqu'elle est jugée par l'opérateur == (), mais elle peut échouer lorsque jugé par compare_exchange_weak à cause de l'objet Le contenu physique sous-jacent peut avoir un alignement de bits ou d'autres représentations logiques qui sont identiques mais qui représentent physiquement des valeurs différentes (par exemple, true et 2 ou 3, ils représentent tous les deux «true» logiquement, mais le les représentations des deux ne sont pas les mêmes en termes physiques).

Contrairement à compare_exchange_strong, la version faible de l'opération de comparaison et d'échange permet à la valeur encapsulée par l'objet atomique d'être la même que le contenu physique du paramètre attendu, mais renvoie toujours false, mais cela est acceptable sous certains algorithmes qui nécessitent une boucle Et les performances de compare_exchange_weak sont meilleures sur certaines plates-formes. Si le jugement de compare_exchange_weak se produit des échecs parasites - même si la valeur encapsulée par l'objet atomique est la même que le contenu physique du paramètre attendu, le résultat de l'opération de jugement est faux, la fonction compare_exchange_weak renvoie faux et la valeur de le paramètre attendu ne changera pas.

Pour certains algorithmes qui n'ont pas besoin d'utiliser des opérations de boucle, il est généralement préférable d'utiliser compare_exchange_strong. En outre, l'ordre de mémoire de cette fonction est spécifié par le paramètre sync. Les conditions facultatives sont les suivantes:
Insérez la description de l'image ici
Veuillez consulter l'exemple suivant:

#include <iostream>       // std::cout
#include <atomic>         // std::atomic
#include <thread>         // std::thread
#include <vector>         // std::vector
 
// a simple global linked list:
struct Node {
    
     
    int value; 
    Node* next; 
};

std::atomic<Node*> list_head(nullptr);
 
void append(int val)
{
    
    
    // append an element to the list
    Node* newNode = new Node{
    
    val, list_head};
 
    // next is the same as: list_head = newNode, but in a thread-safe way:
    while (!list_head.compare_exchange_weak(newNode->next,newNode)) {
    
    }
    // (with newNode->next updated accordingly if some other thread just appended another node)
}
 
int main ()
{
    
    
    // spawn 10 threads to fill the linked list:
    std::vector<std::thread> threads;
    for (int i = 0; i < 10; ++i) 
        threads.push_back(std::thread(append, i));
        
    for (auto& th : threads) 
        th.join();
 
    // print contents:
    for (Node* it = list_head; it!=nullptr; it=it->next)
        std::cout << ' ' << it->value;
 
    std::cout << '\n';
 
    // cleanup:
    Node* it; 
    while (it=list_head) {
    
    
        list_head=it->next; 
        delete it;
    }
 
    return 0;
}

compare_exchange_strong

Insérez la description de l'image ici
Comparez et échangez si la valeur encapsulée (forte) est égale à la valeur spécifiée par le paramètre attendu, si:

  • S'ils sont égaux, remplacez l'ancienne valeur de l'objet atomique par val.
  • S'il n'est pas égal, remplacez attendu par l'ancienne valeur de l'objet atomique. Par conséquent, après avoir appelé cette fonction, si la valeur encapsulée par l'objet atomique n'est pas égale à la valeur spécifiée par le paramètre attendu, le contenu attendu est le ancienne valeur de l'objet atomique.

Cette fonction lit généralement la valeur encapsulée par l'objet atomique. Si la comparaison est vraie (c'est-à-dire que la valeur de l'objet atomique est égale à attendue), l'ancienne valeur de l'objet atomique est remplacée, mais l'opération entière est atomique. Lire et modifier la valeur dans un thread Lorsqu'un objet atomique est utilisé, un autre thread ne peut pas lire ou modifier l'objet atomique.

Dans le cas (2), le choix de l’ordre de mémoire dépend du résultat de l’opération de comparaison. Si le résultat de la comparaison est vrai (c’est-à-dire que la valeur de l’objet atomique est égale à celle attendue), l’ordre de mémoire spécifié par le paramètre succès est sélectionné, sinon le paramètre est sélectionné échec La séquence de mémoire spécifiée.

Notez que cette fonction compare directement la valeur encapsulée par l'objet atomique avec le contenu physique du paramètre attendu. Par conséquent, dans certains cas, l'opération de comparaison de l'objet est égale lorsqu'elle est jugée par l'opérateur == (), mais elle peut échouer lorsque jugé par compare_exchange_weak à cause de l'objet Le contenu physique sous-jacent peut avoir un alignement de bits ou d'autres représentations logiques qui sont identiques mais qui représentent physiquement des valeurs différentes (par exemple, true et 2 ou 3, ils représentent tous les deux «true» logiquement, mais le les représentations des deux ne sont pas les mêmes en termes physiques).

Contrairement à compare_exchange_weak, l'opération de comparaison et d'échange de la version forte n'est pas autorisée (faussement) à renvoyer false, c'est-à-dire que la valeur encapsulée par l'objet atomique est la même que le contenu physique du paramètre attendu, et l'opération de comparaison doit être vrai. Cependant, sur certaines plates-formes, si l'algorithme lui-même a besoin d'une opération de boucle pour vérifier, les performances de compare_exchange_weak seront meilleures.

Par conséquent, pour certains algorithmes qui n'ont pas besoin d'utiliser des opérations de boucle, il est généralement préférable d'utiliser compare_exchange_strong. En outre, l'ordre de mémoire de cette fonction est spécifié par le paramètre sync. Les conditions facultatives sont les suivantes:
Insérez la description de l'image ici
Veuillez consulter l'exemple suivant:

#include <iostream>       // std::cout
#include <atomic>         // std::atomic
#include <thread>         // std::thread
#include <vector>         // std::vector
 
// a simple global linked list:
struct Node {
    
     
    int value; 
    Node* next; 
};
std::atomic<Node*> list_head(nullptr);
 
void append(int val)
{
    
    
    // append an element to the list
    Node* newNode = new Node{
    
    val, list_head};
 
    // next is the same as: list_head = newNode, but in a thread-safe way:
 
    while (!(list_head.compare_exchange_strong(newNode->next, newNode)));
    // (with newNode->next updated accordingly if some other thread just appended another node)
}
 
int main ()
{
    
    
    // spawn 10 threads to fill the linked list:
    std::vector<std::thread> threads;
    for (int i = 0; i < 10; ++i) 
        threads.push_back(std::thread(append, i));

    for (auto& th : threads) 
        th.join();
 
    // print contents:
    for (Node* it = list_head; it!=nullptr; it=it->next)
        std::cout << ' ' << it->value;
 
    std::cout << '\n';
 
    // cleanup:
    Node* it; 
    while (it=list_head) {
    
    
        list_head=it->next; 
        delete it;
    }
 
    return 0;
}

Je suppose que tu aimes

Origine blog.csdn.net/qq_24649627/article/details/114366823
conseillé
Classement