C++:std::atomic
atomic 原子操作:
原子:即最小且不可分割的执行单元,我们知道高级语言一条命令在底层可能分解为多条命令执行,在多线程时,可能会造成无可预料的问题。
如果某些命令的操作是不可分割的,意思就是只有两种状态,完成和未完成,这样就解决了多线程同时访问导致的问题。
优势:解决多线程冲突的方法最早可以通过各种锁来实现,但锁的效率不高,引入了原子操作后可以实现多线程间的无锁同步。
互斥量的加锁一般是针对一个代码段,而原子操作针对的一般都是一个变量。
原子变量既不可复制亦不可移动。
atomic_flag
最简单的原子变量实例
先用这个类型来讲解原子变量。
atomic_flag其实是一个bool类型的原子变量,可以设置该变量为true 或 false,获取该变量的值等。
接口介绍:
ATOMIC_FLAG_INIT:用于给atomic_flag变量赋初值,如果定义后为赋值,则状态是不确定的。被这个赋值后的状态为false。
test_and_set() :接口函数,调用后状态变为true,并返回改变状态前的状态值。
clear() : 接口函数,调用后状态变为false。
应用1:实现自选锁,代码来自cppreference
#include <thread> #include <vector> #include <iostream> #include <atomic> std::atomic_flag lock = ATOMIC_FLAG_INIT; int gcnt = 0; void f(int n) { for (int cnt = 0; cnt < 100; ++cnt) { while (lock.test_and_set(std::memory_order_acquire)) // 获得锁 ; // 自旋 std::cout << "Output from thread " << n << '\n'; gcnt++; lock.clear(std::memory_order_release); // 释放锁 } } int main() { std::vector<std::thread> v; for (int n = 0; n < 10; ++n) { v.emplace_back(f, n); } for (auto& t : v) { t.join(); } std::cout << “All output command count :” << gcnt << std::endl; }
自旋锁的解释:当某一个线程调用‘lock.test_and_set’时,即设置lock的状态为true,当另一个线程进入时,再次调用test_and_set时返回的状态为true,则一直在while循环中不断获取,即实现了等待,知道第一个线程调用clear将状态变为false。
应用2:多线程竞争
以下例子展示五锁实现多线程同步
#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 std::atomic<bool> ready(false); // can be checked without being set std::atomic_flag winner = ATOMIC_FLAG_INIT; // always set when checked void count1m(int id) { while (!ready) { std::this_thread::yield(); } // 等待主线程中设置 ready 为 true. // 如果某个线程率先执行完上面的计数过程,则输出自己的 ID. // 此后其他线程执行 test_and_set 是 if 语句判断为 false, // 因此不会输出自身 ID. if (!winner.test_and_set()) { std::cout << "thread #" << id << " won!\n"; } }; int main() { std::vector<std::thread> threads; for (int i = 1; i <= 1000; ++i) threads.push_back(std::thread(count1m, i)); ready = true; for (auto & th:threads) th.join(); return 0; }
这个程序的输出只会有一个线程输出。
但是如果不使用原子锁则会有多个线程胜出。
bool bwin = false; void count1m(int id) { while (!ready) { std::this_thread::yield(); } // 等待主线程中设置 ready 为 true. //if (!winner.test_and_set()) if (!bwin) { bwin = true; std::cout << "thread #" << id << " won!\n"; } };
如果将线程函数改成普通变量,则输出可能是这样:
atomic<T> 模板类
上述的atomic_flag只是对于bool类型的变量进行原子操作;通过atomic模板类可以对更多的类型进行原子操作。
atomic类本身是禁止复制和拷贝的,但其模板的对象可以直接赋值给atomic类,相当于隐式转换。
eg:
atomic<int> aint1; aint1 = 1; atomic<int> aint2(2); // atomic<int> aint2(aint1); //错误:禁止拷贝构造 // aint2 = aint1; //错误:禁止赋值
is_lock_free() :atomic并不保证模板类是无锁的,所以可以通过这个接口判断是否需要加锁。atomic_flag一定是无锁的。
至于判断无锁的标准可能跟模板类的大小和硬件操作系统有关。
可参考:https://blog.csdn.net/weixin_43705457/article/details/103979660
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
;
T load (memory_order sync = memory_order_seq_cst)
const
volatile
noexcept
;
T load (memory_order sync = memory_order_seq_cst)
const
noexcept
;
bool compare_exchange_weak (T& expected, T val, memory_order sync = memory_order_seq_cst) volatile noexcept; bool compare_exchange_weak (T& expected, T val, memory_order sync = memory_order_seq_cst) noexcept;
用于比较expected的值于atomic内部的值是否相等:
相等:则将val的值赋值给内部的模板对象
不相等:则内部的值赋值给expected
atomic针对整形及指针的优化
提供一些算数运算的操作符,且都是原子操作的。