C++ 多线程同步之原子操作(atomic)实战
💡 学习目标:掌握 C++ 标准库中原子操作的使用方法,理解原子操作与互斥锁的区别,能够在轻量级同步场景中高效解决数据竞争问题。 💡 : 模板的常用接口、原子操作的特性、原子类型与普通类型的性能对比、原子操作的典型应用场景。
C++ 多线程同步中,std::atomic 提供轻量级无锁方案,适用于单变量简单操作。相比互斥锁,原子操作利用 CPU 指令避免上下文切换,减少开销且效率更高。常用接口涵盖赋值、自增、交换及内存序控制,可平衡性能与同步强度。典型场景包括线程退出标志控制及任务计数统计。需注意原子操作仅保证单个操作原子性,复杂逻辑组合仍需互斥锁保护,高竞争场景下需权衡自旋等待成本。

💡 学习目标:掌握 C++ 标准库中原子操作的使用方法,理解原子操作与互斥锁的区别,能够在轻量级同步场景中高效解决数据竞争问题。 💡 : 模板的常用接口、原子操作的特性、原子类型与普通类型的性能对比、原子操作的典型应用场景。
std::atomic在之前的章节我们学习了互斥锁,它通过阻塞线程的方式实现临界区保护。 但互斥锁存在上下文切换开销,在一些简单的同步场景中显得过于笨重。 比如对单个变量的自增、自减、赋值等操作,我们需要一种更轻量级的同步方案——原子操作。
⚠️ 注意事项:原子操作仅适用于单个变量的简单同步,无法替代互斥锁实现复杂临界区的保护。
举个例子,使用互斥锁保护变量自增:
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
int count = 0;
mutex mtx;
void increment() {
for (int i = 0; i < 100000; ++i) {
lock_guard<mutex> lock(mtx);
count++;
}
}
int main() {
thread t1(increment);
thread t2(increment);
t1.join();
t2.join();
cout << "最终 count 值:" << count << endl;
return 0;
}
这段代码虽然能保证线程安全,但每次加锁解锁都会带来额外开销。 原子操作可以在无锁的情况下实现同样的效果,且效率更高。
C++11 标准库在 <atomic> 头文件中提供了 std::atomic 模板类。
它可以将普通类型包装成原子类型,支持原子化的读、写、修改操作。
std::atomic 的核心特性std::atomic 是模板类,可以包装大多数基本数据类型,如 int、bool、long 等。以 std::atomic<int> 为例,常用接口如下:
= 赋值,用 load() 读取值,默认是顺序一致的内存序。fetch_add()(原子自增)、fetch_sub()(原子自减),返回操作前的值。exchange()(原子交换值)、compare_exchange_weak()(比较并交换)。++、--、+=、-= 等重载运算符,使用更便捷。✅ 核心结论:原子操作的重载运算符(如 ++)是对 fetch_add() 的封装,使用起来和普通变量几乎一致。
我们使用 std::atomic<int> 改造上一节的例子,实现无锁同步:
#include <iostream>
#include <thread>
#include <atomic>
using namespace std;
atomic<int> count(0); // 定义原子类型变量
void increment() {
for (int i = 0; i < 100000; ++i) {
count++; // 原子自增操作,无需加锁
}
}
int main() {
thread t1(increment);
thread t2(increment);
t1.join();
t2.join();
cout << "最终 count 值:" << count << endl;
return 0;
}
运行该程序,最终 count 的值稳定等于 200000。
和互斥锁版本相比,这段代码不仅更简洁,而且执行效率更高。
std::atomic 的操作可以指定内存序,用来平衡性能和同步强度。
常用的内存序有三种:
示例:使用松散内存序优化自增操作
void increment() {
for (int i = 0; i < 100000; ++i) {
count.fetch_add(1, memory_order_relaxed);
}
}
在只需要保证原子性的场景下,松散内存序可以显著提升性能。
| 特性 | 原子操作 | 互斥锁 |
|---|---|---|
| 同步粒度 | 仅支持单个变量的原子操作 | 支持任意复杂的临界区代码 |
| 性能开销 | 低(CPU 指令级,无上下文切换) | 高(可能触发上下文切换) |
| 使用复杂度 | 低(类似普通变量,无需手动加解锁) | 高(需手动管理锁的生命周期) |
| 死锁风险 | 无 | 有(不当使用会导致死锁) |
💡 选型技巧:
原子布尔类型 std::atomic<bool> 常用于实现线程的安全退出控制:
#include <iostream>
#include <thread>
#include <atomic>
#include <chrono>
using namespace std;
atomic<bool> is_running(true); // 原子标志位
void worker() {
while (is_running.load()) { // 原子读取标志位
cout << "线程正在运行..." << endl;
this_thread::sleep_for(chrono::milliseconds(500));
}
cout << "线程安全退出" << endl;
}
int main() {
thread t(worker);
this_thread::sleep_for(chrono::seconds(2));
is_running = false; // 原子赋值,通知线程退出
t.join();
cout << "主线程结束" << endl;
return 0;
}
✅ 运行效果:线程运行 2 秒后,检测到标志位变为 false,安全退出。
这个场景用原子操作比互斥锁更轻量、更高效。
使用原子操作实现多线程任务完成情况的统计,无需加锁:
#include <iostream>
#include <thread>
#include <atomic>
#include <vector>
using namespace std;
const int TASK_COUNT = 10;
atomic<int> completed_tasks(0); // 已完成任务数
void task(int id) {
cout << "任务" << id << "开始执行" << endl;
this_thread::sleep_for(chrono::milliseconds(300));
completed_tasks++; // 原子自增,统计完成数
cout << "任务" << id << "执行完毕" << endl;
}
int main() {
vector<thread> threads;
for (int i = 1; i <= TASK_COUNT; ++i) {
threads.emplace_back(task, i);
}
for (auto& t : threads) {
t.join();
}
cout << "所有任务执行完毕,总计完成:" << completed_tasks << "个" << endl;
return 0;
}
运行该程序,最终输出的完成任务数一定等于 10。
这个案例充分体现了原子操作在简单统计场景下的优势。
误用原子操作保护复杂逻辑 原子操作只能保证单个操作的原子性,不能保护多个原子操作的组合。 例如:
// 错误示例:这两个原子操作的组合不是原子的
if (count < 100) {
count++;
}
这个逻辑可能被多个线程打断,需要互斥锁来保护。
std::atomic 是 C++ 标准库的原子类型模板,支持自增、自减、交换等常用原子操作。
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online