一、无锁机制
无锁机制的定义是:
多线程环境下,至少有一个线程能在有限步内完成操作(保证系统整体不饿死) ,即线程不会陷入互相等待的“死锁”中。
换句话说:
- 线程之间不使用传统的互斥锁(
mutex)。 - 线程可能会自旋重试,但不会被阻塞挂起。
- 某个线程可能反复失败 CAS,但整体系统依然“在进展”。
二、atomic相关API
-
条件原子交换 (Compare-and-Exchange, CAS) 这是所有无锁编程的基石,也是最强大、最核心的操作。
-
compare_exchange_weak(expected, desired) -
compare_exchange_strong(expected, desired)-
作用:原子地比较原子变量的当前值与
expected(期望值)。- 如果相等:就用
desired(目标值)替换当前值,并返回true。 - 如果不相等:不做任何修改,但会用原子变量的当前值去更新
expected(这是关键!),并返回false。
- 如果相等:就用
-
weakvs.strong的区别:strong:可靠。如果它返回false,你可以百分百确定是因为值不相等。weak:可能“伪失败” 。它在值相等的情况下,也有可能会偶然地交换失败并返回false。
-
为什么需要
weak:在某些硬件平台上,weak版本的实现可以被编译成更高效的指令。因为它可能伪失败,所以它必须被用在一个循环里。我们之前修复无锁栈push时使用的就是weak版本。
-
-
使用原则:如果你的算法本身就需要一个循环,那么使用
weak可能会带来性能优势。如果你的算法逻辑不需要循环,并且你希望一次就知道结果,那么应该使用strong。 -
使用案例:
void push(const T& val) { Node<T>* newNode = new Node<T>(val); Node<T>* oldHead = head.load(std::memory_order_relaxed); do { newNode->next = oldHead; } while (!head.compare_exchange_weak(oldHead, newNode, std::memory_order_release, std::memory_order_relaxed)); }
-
-
基本存取
这是最基础的两个操作,用于安全地读取和写入原子变量。
-
std::atomic<T>::store(value)- 作用:原子地用
value替换当前值。 - 行为:这是一个单纯的写入操作。
- 作用:原子地用
-
std::atomic<T>::load()- 作用:原子地读取并返回当前值。
- 行为:这是一个单纯的读取操作。
-
-
原子交换
这是一个基础的“读-改-写”(Read-Modify-Write, RMW)操作。
std::atomic<T>::exchange(new_value)- 作用:原子地用
new_value替换当前值,并返回被替换前的旧值
- 作用:原子地用
-
其他功能函数
用于对原子变量进行一些检查相关的操作
-
is_lock_free()- 作用:返回 true 说明该原子类型操作是无锁的,用的是原子指令,返回 false 则是用锁
-
is_trivially_copyable()- 作用: 原子类型是自定义类型,该自定义类型必须可平凡复制(trivially copyable),也就意味着该类型不能有虚函数或虚基类。用来检测是否满足条件。
- 案例:
class A { public: virtual void f() {} }; assert(!std::is_trivially_copyable_v<A>); std::atomic<A> a; // 错误:A 不满足 trivially copyable std::atomic<std::vector<int>> v; // 错误 std::atomic<std::string> s; // 错误
-
-
最基础的原语:
std::atomic_flag
- 唯一被标准保证在所有平台上都无锁的类型。
- 接口极简,主要是
test_and_set()(测试旧值并设置为true)和clear()(设置为false)。 - 它是构建其他同步原语(如自旋锁)的理论基石。
三、泛型编程约束
3.1. 可平凡复制
-
规则:
std::atomic<T>的模板参数T必须是可平凡复制的。 -
原因: CPU 原子指令操作的是纯粹的内存位,不理解 C++ 的构造/析构函数、虚函数表等复杂概念。可平凡复制的类型保证了其内存布局是“朴实”的,可以被硬件安全地按位操作。
-
检测: 可在编译时通过
<type_traits>中的std::is_trivially_copyable_v<T>来检查。 -
失败案例:
class A { virtual void f() {} }; // 以下都会导致编译错误 // std::atomic<A> a; // std::atomic<std::vector<int>> v; // std::atomic<std::string> s;
3.2 自由函数
1. 自由函数 (atomic_xxx)
- 对于每个成员函数
a.func(),几乎都有一个等价的自由函数atomic_func(&a)。 - 主要目的是为了与 C11 的
<stdatomic.h>兼容,因为 C 语言没有成员函数,只能使用指针传递。
2. _explicit 后缀
-
默认的自由函数使用最强的内存顺序
seq_cst。 -
带有
_explicit后缀的版本(如atomic_load_explicit)接受一个额外的memory_order参数,用于性能调优。
3.3 智能指针操作
1. C++20 之前的状况
std::atomic<T>不支持std::shared_ptr<T>。- 但
<memory>头文件提供了一套特殊的自由函数(std::atomic_load,std::atomic_store等)来对非原子的std::shared_ptr变量进行原子操作,作为一种“特例”存在。 -
这些函数是重载,不是模板泛化,因此不会触发std::shared_ptr<int> global_ptr; void update_ptr() { auto new_ptr = std::make_shared<int>(42); std::atomic_store(&global_ptr, new_ptr); } void read_ptr() { auto local = std::atomic_load(&global_ptr); // 使用 local 读取值 }trivially_copyable限制。
2. C++20
- 正式引入了
std::atomic<std::shared_ptr<T>>的完整类模板特化。 - 接口统一:现在可以像其他原子类型一样,直接定义
std::atomic<std::shared_ptr<T>> p;并使用p.load(),p.store()等成员函数。 - 保证无锁:标准强制要求其实现必须是无锁的。
-
std::atomic<std::shared_ptr<int>> p; p.store(std::make_shared<int>(123)); // 原子存储 auto v = p.load(); // 原子读取