文章目录
5.2 原子操作和原子类型
在多线程程序里,有个非常重要的概念叫 原子操作。
所谓“原子”,意思就是:
👉 这个操作要么完整执行,要么完全没发生,绝不会出现“执行了一半”的情况。
打个比方:
- 原子操作 就像你开关灯,要么灯是开,要么灯是关,中间不会有人看到“半亮半暗”的状态。
- 非原子操作 就可能像换灯泡,你手刚拧下灯泡一半,别人看见灯既没完全亮,也没完全灭,结果导致“奇怪的中间状态”。
如果线程之间访问同一个变量时不是原子操作,就可能出现 数据竞争,从而导致未定义行为。
5.2.1 标准原子类型
C++ 提供了 <atomic>
头文件,里面有一系列 原子类型。
比如:
std::atomic<int>
→ 原子整型std::atomic<bool>
→ 原子布尔std::atomic<T*>
→ 原子指针std::atomic_flag
→ 最简单的原子布尔标志
这些类型内部会用硬件的原子指令(如果平台支持),否则退而求其次用锁模拟。你可以通过 is_lock_free()
来检查某个原子类型是不是无锁实现。
⚡ 注意:只有
std::atomic_flag
是保证无锁的。
5.2.2 std::atomic_flag
—— 最简单的原子类型
这是最简单的原子类型,只能做三件事:
- 初始化(必须用
ATOMIC_FLAG_INIT
) test_and_set()
:设置为 true,并返回旧值clear()
:清除为 false
它经常用来实现一个 自旋锁 (spinlock)。
下面我们写个完整的例子:多个线程一起往一个 std::vector<int>
里添加数据,但用 spinlock_mutex
来保护。
#include <atomic>
#include <iostream>
#include <thread>
#include <vector>
class spinlock_mutex {
std::atomic_flag flag;
public:
spinlock_mutex() : flag(ATOMIC_FLAG_INIT) {}
void lock() {
while (flag.test_and_set(std::memory_order_acquire)); // 一直忙等,直到获取锁
}
void unlock() {
flag.clear(std::memory_order_release);
}
};
spinlock_mutex my_lock;
std::vector<int> shared_data;
void worker(int id) {
for (int i = 0; i < 5; i++) {
my_lock.lock();
shared_data.push_back(id * 10 + i);
my_lock.unlock();
}
}
int main() {
std::thread t1(worker, 1);
std::thread t2(worker, 2);
t1.join();
t2.join();
for (int v : shared_data) {
std::cout << v << " ";
}
std::cout << std::endl;
}
运行后你会看到两个线程都安全地往 shared_data
里写入数据。
5.2.3 std::atomic<bool>
std::atomic<bool>
是更通用的布尔原子类型,它支持更多操作,比如:
store()
写入load()
读取exchange()
交换compare_exchange_weak()
/compare_exchange_strong()
比较并交换(CAS)
示例:
两个线程竞争某个布尔开关,谁先把它从 false
改成 true
,谁就“赢了”。
#include <atomic>
#include <iostream>
#include <thread>
std::atomic<bool> ready(false);
void try_to_win(int id) {
bool expected = false;
if (ready.compare_exchange_strong(expected, true)) {
std::cout << "Thread " << id << " got the flag!\n";
} else {
std::cout << "Thread " << id << " lost...\n";
}
}
int main() {
std::thread t1(try_to_win, 1);
std::thread t2(try_to_win, 2);
t1.join();
t2.join();
}
每次运行,只有一个线程会赢,另一个会失败。
5.2.4 std::atomic<T*>
—— 原子指针
它支持指针运算,比如 fetch_add()
可以让指针往后移动。
例子:多个线程安全地遍历数组。
#include <atomic>
#include <iostream>
#include <thread>
struct Foo {
int value;
};
Foo arr[5] = { {1}, {2}, {3}, {4}, {5} };
std::atomic<Foo*> ptr(arr);
void worker(int id) {
Foo* old = ptr.fetch_add(1); // 原子地取出旧值并让指针+1
if (old < arr + 5) {
std::cout << "Thread " << id << " got value " << old->value << "\n";
}
}
int main() {
std::thread threads[5];
for (int i = 0; i < 5; i++) {
threads[i] = std::thread(worker, i+1);
}
for (auto& t : threads) t.join();
}
结果:每个线程取到数组的一个元素,互不冲突。
5.2.5 原子整型
常用的操作:
fetch_add()
、fetch_sub()
fetch_and()
、fetch_or()
、fetch_xor()
- 自增
++
/ 自减--
我们写个线程安全的计数器:
#include <atomic>
#include <iostream>
#include <thread>
std::atomic<int> counter(0);
void increment(int id) {
for (int i = 0; i < 1000; i++) {
counter++;
}
}
int main() {
std::thread t1(increment, 1);
std::thread t2(increment, 2);
t1.join();
t2.join();
std::cout << "Final counter = " << counter.load() << std::endl;
}
最后输出 2000
,说明没有丢计数。
5.2.6 通用 std::atomic<T>
如果你想要对自定义类型使用原子操作,可以用 std::atomic<T>
,但限制比较多:
- 不能有虚函数、虚继承
- 必须能用
memcpy
拷贝 - 比较时是按字节比较(memcmp),不是用户定义的
operator==
例子:定义一个简单的 struct
,然后做原子交换。
#include <atomic>
#include <iostream>
#include <thread>
struct Point {
int x, y;
};
std::atomic<Point> ap;
void worker(int id) {
Point p{id, id*2};
Point old = ap.exchange(p); // 原子替换
std::cout << "Thread " << id << " set (" << p.x << "," << p.y
<< "), old was (" << old.x << "," << old.y << ")\n";
}
int main() {
ap.store({0,0});
std::thread t1(worker, 1);
std::thread t2(worker, 2);
t1.join();
t2.join();
}
你会看到两个线程交替写入和读取 Point
。
5.2.7 非成员函数形式
C++ 还提供了和成员函数等价的 非成员函数,主要是为了兼容 C。比如:
#include <atomic>
#include <iostream>
int main() {
std::atomic<int> a(10);
int x = std::atomic_load(&a);
std::cout << "Load = " << x << std::endl;
std::atomic_store(&a, 20);
std::cout << "After store = " << a.load() << std::endl;
}
总结
std::atomic_flag
:最简单的无锁布尔,用来实现自旋锁。std::atomic<bool>
:更通用的布尔原子,支持 CAS。std::atomic<T*>
:指针的原子操作,支持加减。- 原子整型:支持加减、位运算、自增自减,常用于计数器。
- 通用
std::atomic<T>
:能对简单的用户定义类型做原子操作。 - 非成员函数:提供 C 兼容的接口。
好嘞 👍 我帮你把这份代码逐行加上 中文注释,而且会解释 为什么运行会得到这样的结果。这样你在学习的时候可以对照着源码 + 注释理解。
带详细中文注释的版本
#include <atomic> // 原子操作相关的头文件
#include <iostream> // 标准输入输出
#include <thread> // 多线程支持
#include <vector> // 动态数组容器
// ======================================================
// 例子 1: atomic_flag 实现自旋锁
// ======================================================
// 用 atomic_flag 实现一个简单的自旋锁
class spinlock_mutex {
std::atomic_flag flag; // 原子标志,用来表示锁是否被占用
public:
// 构造函数:用 clear() 把标志位初始化为 false (未加锁)
spinlock_mutex() { flag.clear(); }
// 加锁操作
void lock() {
// test_and_set() 会把 flag 设为 true,并返回旧值
// 如果旧值是 true,说明锁被占用,就继续忙等
while (flag.test_and_set(std::memory_order_acquire));
}
// 解锁操作
void unlock() {
// clear() 把 flag 设为 false,表示释放锁
flag.clear(std::memory_order_release);
}
};
spinlock_mutex my_lock; // 定义一个自旋锁
std::vector<int> shared_data; // 共享数据,多个线程要往里面写入
// 工作者线程:往 shared_data 里写 5 个数
void worker_flag(int id) {
for (int i = 0; i < 5; i++) {
my_lock.lock(); // 加锁,保证只有一个线程能进入
shared_data.push_back(id * 10 + i);// 写数据
my_lock.unlock(); // 解锁
}
}
// 演示 atomic_flag
void demo_atomic_flag() {
shared_data.clear(); // 清空数据
std::thread t1(worker_flag, 1);
std::thread t2(worker_flag, 2);
t1.join();
t2.join();
std::cout << "[atomic_flag] shared_data: ";
for (int v : shared_data) std::cout << v << " ";
std::cout << "\n";
// 结果:两个线程安全地写入数据,不会崩溃或数据错乱
// 但输出顺序可能不同,比如 "10 11 12 13 14 20 21 22 23 24"
}
// ======================================================
// 例子 2: atomic<bool> CAS 操作
// ======================================================
std::atomic<bool> ready(false); // 一个原子布尔值,初始为 false
// 尝试争夺布尔开关的线程函数
void try_to_win(int id) {
bool expected = false; // 期望值
// compare_exchange_strong():如果 ready == expected,就把它设为 true
if (ready.compare_exchange_strong(expected, true)) {
std::cout << "Thread " << id << " got the flag!\n"; // 成功的线程
}
else {
std::cout << "Thread " << id << " lost...\n"; // 失败的线程
}
}
void demo_atomic_bool() {
ready.store(false); // 每次测试前重置为 false
std::thread t1(try_to_win, 1);
std::thread t2(try_to_win, 2);
t1.join();
t2.join();
// 结果:只有一个线程能成功把 false 改为 true,另一个会失败
// 每次运行可能 t1 赢,也可能 t2 赢
}
// ======================================================
// 例子 3: atomic<T*> 指针操作
// ======================================================
struct Foo {
int value;
};
Foo arr[5] = { {1}, {2}, {3}, {4}, {5} }; // 一个数组
std::atomic<Foo*> ptr(arr); // 原子指针,初始指向 arr[0]
// 工作者线程:原子地获取并移动指针
void worker_ptr(int id) {
Foo* old = ptr.fetch_add(1); // 返回旧指针,并让指针+1
if (old < arr + 5) { // 确保没越界
std::cout << "Thread " << id << " got value " << old->value << "\n";
}
}
void demo_atomic_pointer() {
ptr.store(arr); // 重置指针到数组开头
std::thread threads[5];
for (int i = 0; i < 5; i++) {
threads[i] = std::thread(worker_ptr, i + 1);
}
for (auto& t : threads) t.join();
// 结果:5 个线程各自拿到 arr 里的不同元素,互不冲突
// 输出顺序不固定,但每个值 1~5 都会被打印一次
}
// ======================================================
// 例子 4: 原子整型计数器
// ======================================================
std::atomic<int> counter(0); // 原子整型计数器,初始为 0
// 每个线程循环自增 1000 次
void increment(int id) {
for (int i = 0; i < 1000; i++) {
counter++; // 原子操作,保证不会丢计数
}
}
void demo_atomic_int() {
counter.store(0); // 重置为 0
std::thread t1(increment, 1);
std::thread t2(increment, 2);
t1.join();
t2.join();
std::cout << "[atomic<int>] Final counter = " << counter.load() << "\n";
// 结果:最终 counter = 2000,证明两个线程的自增操作是安全的
}
// ======================================================
// 例子 5: 通用 atomic<T> 用户自定义类型
// ======================================================
struct Point {
int x, y;
};
std::atomic<Point> ap; // 原子存储一个结构体 Point
// 工作者线程:用 exchange() 替换值
void worker_point(int id) {
Point p{ id, id * 2 }; // 新值
Point old = ap.exchange(p); // 原子替换,返回旧值
std::cout << "Thread " << id << " set (" << p.x << "," << p.y
<< "), old was (" << old.x << "," << old.y << ")\n";
}
void demo_atomic_struct() {
ap.store({ 0,0 }); // 初始值 (0,0)
std::thread t1(worker_point, 1);
std::thread t2(worker_point, 2);
t1.join();
t2.join();
// 结果:两个线程依次替换结构体
// 可能输出:
// Thread 1 set (1,2), old was (0,0)
// Thread 2 set (2,4), old was (1,2)
}
// ======================================================
// 例子 6: 非成员函数接口
// ======================================================
void demo_nonmember_functions() {
std::atomic<int> a(10); // 原子整型,初始值 10
int x = std::atomic_load(&a); // 非成员函数 load
std::cout << "[nonmember] Load = " << x << "\n"; // 输出 10
std::atomic_store(&a, 20); // 非成员函数 store
std::cout << "[nonmember] After store = " << a.load() << "\n"; // 输出 20
}
// ======================================================
// 主函数:依次运行所有 demo
// ======================================================
int main() {
std::cout << "===== Atomic Examples =====\n";
demo_atomic_flag(); // 演示自旋锁
demo_atomic_bool(); // 演示 CAS
demo_atomic_pointer(); // 演示原子指针
demo_atomic_int(); // 演示原子计数器
demo_atomic_struct(); // 演示原子结构体
demo_nonmember_functions();// 演示非成员函数接口
return 0;
}
总结(为什么结果是这样)
- atomic_flag → 自旋锁保护共享数据,保证两个线程写入 vector 不会乱。
- atomic → 只有一个线程能把
false → true
,另一个失败。 - *atomic<T>** → 每个线程安全地拿到数组里不同的元素。
- atomic → 自增 2000 次不会丢计数。
- atomic → 两个线程依次替换结构体,能看到前一个值。
- 非成员函数 → load/store 和成员函数效果一样,只是另一种写法。
要不要我再帮你画一张 流程图 (mermaid),把这几个例子之间的知识点关系梳理成一个知识地图?
好嘞,我给你画一张 原子操作示例知识关系图,用 Mermaid 语法表示,这样能直观看到各个例子之间的关系和应用场景。
说明:
- B 自旋锁:用
atomic_flag
来保护共享资源,保证多线程安全。 - C CAS 操作:用
compare_exchange_strong
做一次性竞争。 - D 原子指针:多线程安全地移动指针,避免数组越界冲突。
- E 原子计数器:线程安全自增,保证累加结果正确。
- F 原子结构体:可以直接对结构体类型进行原子操作。
- G 非成员函数接口:提供了
atomic_load
和atomic_store
的替代写法,功能和成员函数一致。
如果你愿意,我可以帮你把这张 Mermaid 图 升级为带示例运行结果的可视化流程图,这样每个节点不仅知道功能,还能直接看到输出示例,更直观学习。
你希望我做吗?