C ++ Concurrency Guide std :: atomic

Grundlegende Einführung in std :: atomic

std :: atomic ist eine Vorlagenklasse. Ein atomares Objekt mit dem Vorlagentyp T kapselt einen Wert vom Typ T.

Template-Struktur atomar;

Das Hauptmerkmal von Objekten vom atomaren Typ ist, dass der Zugriff von verschiedenen Threads keine Datenrennen verursacht. Daher ist der Zugriff auf ein atomares Objekt von verschiedenen Threads aus ein genau definiertes Verhalten. Bei nichtatomaren Typen führt der gleichzeitige Zugriff auf ein Objekt (wenn keine Synchronisierungsoperation ausgeführt wird) im Allgemeinen zu einem undefinierten Verhalten.

std :: atomarer Konstruktor

Der Konstruktor von std :: atomic lautet wie folgt:
Fügen Sie hier eine Bildbeschreibung ein

  1. Der Standardkonstruktor, das vom Standardkonstruktor erstellte std :: atomic-Objekt, befindet sich im nicht initialisierten Zustand, und das std :: atomic-Objekt im nicht initialisierten Zustand kann mit der Funktion atomic_init initialisiert werden.
  2. Initialisieren Sie den Konstruktor, initialisieren Sie ein std :: atomic-Objekt nach Typ T.
  3. Der Kopierkonstruktor ist deaktiviert.

Betrachten Sie das folgende Beispiel:

#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;
}

std :: atomic :: operator = () Funktion

Die Zuweisungsoperationsfunktion von std :: atomic ist wie folgt definiert: Es
Fügen Sie hier eine Bildbeschreibung ein
ist ersichtlich, dass die gewöhnliche Zuweisungskopieroperation deaktiviert wurde. Eine Variable vom Typ T kann jedoch der entsprechenden atomaren Typvariablen zugewiesen werden (entspricht der impliziten Konvertierung), die Operation ist atomar und die Speicherreihenfolge (Speicherreihenfolge) verwendet standardmäßig die sequentielle Konsistenz (std :: memory_order_seq_cst), falls erforderlich spezifizieren Verwenden Sie für andere Speichersequenzen 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;
}

Grundlegende Operationen vom Typ std :: atomic

is_lock_free

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

Bestimmen Sie, ob das std :: atomic-Objekt sperrfreie Eigenschaften aufweist. Wenn ein Objekt die Sperrfunktion erfüllt, wird kein Thread blockiert, wenn mehrere Threads auf das Objekt zugreifen. (Kann eine Art Transaktionsspeichermethode verwenden, um sperrfreie Funktionen zu erzielen).

Geschäft

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;

Um den gekapselten Wert zu ändern, kopiert die Funktion std :: atomic :: store den Parameter val vom Typ T in den vom atomaren Objekt gekapselten Wert. T ist der Vorlagenparameter std :: atomic class. Darüber hinaus gibt der Parameter sync die Speicherreihenfolge an.
Fügen Sie hier eine Bildbeschreibung ein
Folgende Werte sind möglich: Siehe folgendes Beispiel:

#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;
}

Belastung

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

Lesen Sie den gekapselten Wert, die Parametersynchronisierung legt die Speicherreihenfolge (Speicherreihenfolge) fest. Mögliche Werte sind:
Fügen Sie hier eine Bildbeschreibung ein
Siehe das obige Speicherbeispiel.

Operator T.

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

Ähnlich wie bei der Ladefunktion wird auch der gekapselte Wert gelesen. Der Operator T () ist eine Typumwandlungsoperation. Die Standardspeicherreihenfolge lautet std :: memory_order_seq_cst. Wenn Sie andere Speicherreihenfolgen angeben müssen, sollten Sie die Funktion load () verwenden .
Betrachten Sie das folgende Beispiel:

#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;
}

Austausch-

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;

Lesen und ändern Sie den gekapselten Wert. Exchange ersetzt den durch val angegebenen Wert durch den Wert, der zuvor vom atomaren Objekt gekapselt wurde, und gibt den Wert zurück, der zuvor vom atomaren Objekt gekapselt wurde. Der gesamte Prozess ist atomar (daher wird die Austauschoperation auch als Lesen bezeichnet - Änderungsschreiboperation).
Der Synchronisierungsparameter gibt die Speicherreihenfolge (Speicherreihenfolge) an. Folgende Werte sind möglich: Im folgenden
Fügen Sie hier eine Bildbeschreibung ein
Beispiel zählt jeder Thread bis 1 MB, und der Thread, der die Zählaufgabe abschließt, druckt zuerst seine eigene 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

Fügen Sie hier eine Bildbeschreibung ein
Vergleichen und tauschen Sie aus, ob der gekapselte Wert (schwach) dem Wert entspricht, der durch den erwarteten Parameter angegeben wird, wenn:

  • Wenn sie gleich sind, ersetzen Sie den alten Wert des Atomobjekts durch val.
  • Wenn es nicht gleich ist, ersetzen Sie "Erwartet" durch den alten Wert des Atomobjekts. Wenn nach dem Aufrufen dieser Funktion der vom Atomobjekt eingekapselte Wert nicht dem Wert entspricht, der durch den erwarteten Parameter angegeben wird, ist der erwartete Inhalt der alter Wert des Atomobjekts.

Diese Funktion liest normalerweise den vom Atomobjekt eingekapselten Wert. Wenn der Vergleich wahr ist (dh der Wert des Atomobjekts entspricht dem erwarteten Wert), wird der alte Wert des Atomobjekts ersetzt, aber die gesamte Operation ist atomar. Lesen und Ändern des Werts in einem Thread Wenn ein Atomobjekt verwendet wird, kann ein anderer Thread das Atomobjekt nicht lesen oder ändern.

Im Fall (2) hängt die Wahl der Speicherreihenfolge vom Ergebnis der Vergleichsoperation ab. Wenn das Vergleichsergebnis wahr ist (dh der Wert des atomaren Objekts ist gleich wie erwartet), wird die Speicherreihenfolge durch den Parameter angegeben Erfolg wird ausgewählt, andernfalls wird der Parameter als Fehler ausgewählt. Die angegebene Speichersequenz.

Beachten Sie, dass diese Funktion den vom atomaren Objekt eingekapselten Wert direkt mit dem physischen Inhalt des erwarteten Parameters vergleicht. Daher ist in einigen Fällen die Vergleichsoperation des Objekts gleich, wenn sie vom Operator == () beurteilt wird, sie kann jedoch fehlschlagen, wenn beurteilt durch compare_exchange_weak aufgrund des Objekts Der zugrunde liegende physische Inhalt kann eine Bitausrichtung oder andere logische Darstellungen aufweisen, die gleich sind, aber physikalisch unterschiedliche Werte darstellen (z. B. true und 2 oder 3, beide repräsentieren logisch "true", aber die Darstellungen der beiden sind physikalisch nicht gleich).

Im Gegensatz zu compare_exchange_strong ermöglicht die schwache Version der Vergleichs- und Austauschoperation, dass der vom atomaren Objekt eingekapselte Wert dem physischen Inhalt des erwarteten Parameters entspricht, aber immer noch false zurückgibt. Dies ist jedoch unter bestimmten Algorithmen akzeptabel, die eine Schleife erfordern Operationen. Und die Leistung von compare_exchange_weak ist auf einigen Plattformen besser. Wenn bei der Beurteilung von compare_exchange_weak falsche Fehler auftreten - selbst wenn der vom atomaren Objekt eingekapselte Wert dem physischen Inhalt des erwarteten Parameters entspricht, ist das Ergebnis der Beurteilungsoperation false, die Funktion compare_exchange_weak gibt false zurück und der Wert von Der erwartete Parameter ist nicht Wird sich ändern.

Für einige Algorithmen, die keine Schleifenoperationen verwenden müssen, ist es normalerweise besser, compare_exchange_strong zu verwenden. Darüber hinaus wird die Speicherreihenfolge dieser Funktion durch den Synchronisierungsparameter festgelegt. Die optionalen Bedingungen lauten wie folgt:
Fügen Sie hier eine Bildbeschreibung ein
Siehe folgendes Beispiel:

#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

Fügen Sie hier eine Bildbeschreibung ein
Vergleichen und tauschen Sie aus, ob der gekapselte Wert (stark) dem Wert entspricht, der durch den erwarteten Parameter angegeben wird, wenn:

  • Wenn sie gleich sind, ersetzen Sie den alten Wert des Atomobjekts durch val.
  • Wenn es nicht gleich ist, ersetzen Sie "Erwartet" durch den alten Wert des Atomobjekts. Wenn nach dem Aufrufen dieser Funktion der vom Atomobjekt eingekapselte Wert nicht dem Wert entspricht, der durch den erwarteten Parameter angegeben wird, ist der erwartete Inhalt der alter Wert des Atomobjekts.

Diese Funktion liest normalerweise den vom Atomobjekt eingekapselten Wert. Wenn der Vergleich wahr ist (dh der Wert des Atomobjekts entspricht dem erwarteten Wert), wird der alte Wert des Atomobjekts ersetzt, aber die gesamte Operation ist atomar. Lesen und Ändern des Werts in einem Thread Wenn ein Atomobjekt verwendet wird, kann ein anderer Thread das Atomobjekt nicht lesen oder ändern.

Im Fall (2) hängt die Wahl der Speicherreihenfolge vom Ergebnis der Vergleichsoperation ab. Wenn das Vergleichsergebnis wahr ist (dh der Wert des atomaren Objekts ist gleich wie erwartet), wird die Speicherreihenfolge durch den Parameter angegeben Erfolg wird ausgewählt, andernfalls wird der Parameter als Fehler ausgewählt. Die angegebene Speichersequenz.

Beachten Sie, dass diese Funktion den vom atomaren Objekt eingekapselten Wert direkt mit dem physischen Inhalt des erwarteten Parameters vergleicht. Daher ist in einigen Fällen die Vergleichsoperation des Objekts gleich, wenn sie vom Operator == () beurteilt wird, sie kann jedoch fehlschlagen, wenn beurteilt durch compare_exchange_weak aufgrund des Objekts Der zugrunde liegende physische Inhalt kann eine Bitausrichtung oder andere logische Darstellungen aufweisen, die gleich sind, aber physikalisch unterschiedliche Werte darstellen (z. B. true und 2 oder 3, beide repräsentieren logisch "true", aber die Darstellungen der beiden sind physikalisch nicht gleich).

Im Gegensatz zu compare_exchange_weak darf die Vergleichs- und Austauschoperation der starken Version nicht (fälschlicherweise) false zurückgeben, dh der vom atomaren Objekt eingekapselte Wert entspricht dem physischen Inhalt des erwarteten Parameters und der Vergleichsoperation muss wahr sein. Auf einigen Plattformen ist die Leistung von compare_exchange_weak jedoch besser, wenn der Algorithmus selbst eine Schleifenoperation zur Überprüfung benötigt.

Daher ist es für einige Algorithmen, die keine Schleifenoperationen verwenden müssen, normalerweise besser, compare_exchange_strong zu verwenden. Darüber hinaus wird die Speicherreihenfolge dieser Funktion durch den Synchronisierungsparameter festgelegt. Die optionalen Bedingungen lauten wie folgt:
Fügen Sie hier eine Bildbeschreibung ein
Siehe folgendes Beispiel:

#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;
}

Ich denke du magst

Origin blog.csdn.net/qq_24649627/article/details/114366823
Empfohlen
Rangfolge