C++ Concurrency Guide std::atomic

Basic introduction to std::atomic

std::atomic is a template class. An atomic object with template type T encapsulates a value of type T.

template struct atomic;

The main feature of atomic type objects is that access from different threads does not cause data races. Therefore, accessing an atomic object from different threads is a well-defined behavior, and generally for non-atomic types, concurrent access to an object (if no synchronization operation is performed) will result in undefined behavior.

std::atomic constructor

The constructor of std::atomic is as follows:
Insert picture description here

  1. The default constructor, the std::atomic object created by the default constructor is in the uninitialized state, and the std::atomic object in the uninitialized state can be initialized by the atomic_init function.
  2. Initialize the constructor, initialize a std::atomic object by type T.
  3. The copy constructor is disabled.

Consider the following example:

#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=() function

The assignment operation function of std::atomic is defined as follows: It
Insert picture description here
can be seen that the ordinary assignment copy operation has been disabled. But a variable of type T can be assigned to the corresponding atomic type variable (equivalent to implicit conversion). The operation is atomic, and the memory order (Memory Order) defaults to sequential consistency (std::memory_order_seq_cst), if you need to specify For other memory sequences, use 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;
}

Basic std::atomic type operations

is_lock_free

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

Determine whether the std::atomic object has lock-free characteristics. If an object satisfies the lock-free feature, it will not cause thread blocking when multiple threads access the object. (May use some kind of transactional memory method to achieve lock-free features).

store

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;

To modify the encapsulated value, the std::atomic::store function copies the parameter val of type T to the value encapsulated by the atomic object. T is the std::atomic class template parameter. In addition, the parameter sync specifies Memory Order, and the possible values ​​are as follows:
Insert picture description here
Please see the following example:

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

load

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

Read the encapsulated value, parameter sync sets the memory order (Memory Order), possible values ​​are as follows:
Insert picture description here
please see the store example above.

operator T

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

Similar to the load function, it also reads the encapsulated value. operator T() is a type-cast operation. The default memory order is std::memory_order_seq_cst. If you need to specify other memory orders, you should use load() function.
Consider the following example:

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

exchange

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;

Read and modify the encapsulated value, exchange will replace the value specified by val with the value encapsulated by the atomic object before, and return the value encapsulated by the atomic object before. The whole process is atomic (so the exchange operation is also called read- modify-write operation).
The sync parameter specifies the Memory Order. The possible values ​​are as follows:
Insert picture description here
Please see the following example, each thread counts to 1M, and the thread that completes the counting task first prints its own 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

Insert picture description here
Compare and exchange whether the encapsulated value (weak) is equal to the value specified by the parameter expected, if:

  • If they are equal, replace the old value of the atomic object with val.
  • If it is not equal, replace expected with the old value of the atomic object. Therefore, after calling this function, if the value encapsulated by the atomic object is not equal to the value specified by the parameter expected, the content in expected is the old value of the atomic object.

This function usually reads the value encapsulated by the atomic object. If the comparison is true (that is, the value of the atomic object is equal to expected), the old value of the atomic object is replaced, but the entire operation is atomic. Read and modify the value in a thread When an atomic object is used, another thread cannot read or modify the atomic object.

In the case (2), the choice of memory order depends on the result of the comparison operation. If the comparison result is true (that is, the value of the atomic object is equal to expected), the memory order specified by the parameter success is selected, otherwise the parameter is selected failure The specified memory sequence.

Note that this function directly compares the value encapsulated by the atomic object with the physical content of the parameter expected. Therefore, in some cases, the comparison operation of the object is equal when judged by operator==(), but it may fail when judged by compare_exchange_weak, because the object The underlying physical content may have bit alignment or other logical representations that are the same but physically represent different values ​​(for example, true and 2 or 3, they both represent "true" logically, but the representations of the two are not the same in physical terms) .

Unlike compare_exchange_strong, the weak version of the compare-and-exchange operation allows the value encapsulated by the atomic object to be the same as the physical content of the parameter expected, but still returns false, but this is acceptable under certain algorithms that require loop operations. And the performance of compare_exchange_weak is better on some platforms. If the judgment of compare_exchange_weak does occur spurious failures—even if the value encapsulated by the atomic object is the same as the physical content of the parameter expected, the result of the judgment operation is false, the compare_exchange_weak function returns false, and the value of the parameter expected is not Will change.

For some algorithms that do not need to use loop operations, it is usually better to use compare_exchange_strong. In addition, the memory order of this function is specified by the sync parameter. The optional conditions are as follows:
Insert picture description here
Please see the following example:

#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

Insert picture description here
Compare and exchange whether the encapsulated value (strong) is equal to the value specified by the parameter expected, if:

  • If they are equal, replace the old value of the atomic object with val.
  • If it is not equal, replace expected with the old value of the atomic object. Therefore, after calling this function, if the value encapsulated by the atomic object is not equal to the value specified by the parameter expected, the content in expected is the old value of the atomic object.

This function usually reads the value encapsulated by the atomic object. If the comparison is true (that is, the value of the atomic object is equal to expected), the old value of the atomic object is replaced, but the entire operation is atomic. Read and modify the value in a thread When an atomic object is used, another thread cannot read or modify the atomic object.

In the case (2), the choice of memory order depends on the result of the comparison operation. If the comparison result is true (that is, the value of the atomic object is equal to expected), the memory order specified by the parameter success is selected, otherwise the parameter is selected failure The specified memory sequence.

Note that this function directly compares the value encapsulated by the atomic object with the physical content of the parameter expected. Therefore, in some cases, the comparison operation of the object is equal when judged by operator==(), but it may fail when judged by compare_exchange_weak, because the object The underlying physical content may have bit alignment or other logical representations that are the same but physically represent different values ​​(for example, true and 2 or 3, they both represent "true" logically, but the representations of the two are not the same in physical terms) .

Unlike compare_exchange_weak, the compare-and-exchange operation of the strong version is not allowed (spuriously) to return false, that is, the value encapsulated by the atomic object is the same as the physical content of the parameter expected, and the comparison operation must be true. However, on some platforms, if the algorithm itself needs a loop operation to check, the performance of compare_exchange_weak will be better.

Therefore, for some algorithms that do not need to use loop operations, it is usually better to use compare_exchange_strong. In addition, the memory order of this function is specified by the sync parameter. The optional conditions are as follows:
Insert picture description here
Please see the following example:

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

Guess you like

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