文章目录
在现代高性能多线程编程中,如何高效、安全地处理共享数据是一个关键问题。std::atomic
作为 C++ 标准库提供的一种无锁线程安全工具,因其性能优越和易用性而备受推崇。本文将深入探讨 std::atomic
的特性、使用方法及其在实际开发中的应用场景,帮助读者全面掌握这一工具。
atomic_6">为什么选择 std::atomic?
在多线程环境下,数据共享和修改可能导致竞争条件。传统的 std::mutex
可以通过加锁实现线程安全,但由于其重量级的锁机制,可能带来显著的性能开销。
相比之下,std::atomic
具有以下显著优势:
- 线程安全性:所有操作均为原子性,避免数据竞争。
- 无锁机制:通过硬件支持的原子指令,消除了线程上下文切换的开销。
- 内存序模型:灵活的内存序控制,满足不同场景下的性能与一致性需求。
适用场景包括:
- 简单变量的多线程读写,如计数器、标志位。
- 替代频繁加锁的场景,以优化性能。
atomic__23">std::atomic 的主要特性
1. 支持多种操作
- 基本操作:
load
和store
实现共享变量的读取和写入。 - 数学运算:
fetch_add
和fetch_sub
提供原子加减功能。 - 比较并交换:
compare_exchange_weak
和compare_exchange_strong
用于实现条件更新。 - 高效等待与通知:通过
wait
、notify_one
和notify_all
实现线程间的高效协作。
2. 灵活的内存序
提供从 memory_order_relaxed
到 memory_order_seq_cst
的多种内存序控制,可根据场景需求在性能与一致性之间灵活取舍。
3. 支持多种类型
适用于基础类型(如 int
和 bool
),也可通过特化支持自定义类型。
atomic__42">实践篇:std::atomic 的使用场景
环境要求
本文中的代码需要编译器支持 C++20 标准. 本文在
- GCC 13.2 上面测试通过
- Clang 18.1 上面测试通过
atomic__51">场景 1:如何用 std::atomic
实现线程安全计数器
示例需求
实现一个计数器,支持以下操作:
Increase()
:将计数器加 1,并返回更新后的值。IncreaseBy(int)
:增加指定值,并返回更新后的值。DecreaseBy(int)
:减少指定值,并返回更新后的值。Get()
:返回当前计数器的值。Reset()
:将计数器重置为初始值。
atomic__63">使用 std::atomic
的高效实现
#include <atomic>
#include <iostream>class Counter {public:Counter(int id) : counter_(id) {}void Reset(int cnt) { counter_.store(cnt); }int Get() { return counter_.load(); }int Increase() { return counter_++; }int IncreaseBy(int cnt) { return counter_.fetch_add(cnt) + cnt; }int DecreaseBy(int cnt) { return counter_.fetch_sub(cnt) - cnt; }private:std::atomic<int> counter_;
};int main() {Counter counter(0);counter.Increase();counter.IncreaseBy(10);counter.DecreaseBy(5);std::cout << "Counter: " << counter.Get() << std::endl;return 0;
}
在 Compiler Explorer 上查看.
场景 2:多线程竞争同一任务
示例需求
有一些特定的任务需要有一个线程来执行,并且需要保证只有一个线程能够成功。这时,我们可以使用 std::atomic
来实现。
示例代码
#include <atomic>
#include <chrono>
#include <iostream>
#include <random>
#include <thread>
#include <vector>std::atomic<unsigned long int> leader_id(-1);void try_to_be_leader() {// 生成一个 100 到 1000 毫秒之间的随机睡眠时间std::random_device rd;std::mt19937 gen(rd());std::uniform_int_distribution<> dis(100, 1000);int sleep_duration = dis(gen);// 休眠随机时间, 模拟线程执行任务std::this_thread::sleep_for(std::chrono::milliseconds(sleep_duration));auto thread_id = std::this_thread::get_id();unsigned long int expected = -1;// 尝试成为 leaderif (leader_id.compare_exchange_strong(expected, std::hash<std::thread::id>{}(thread_id))) {std::cout << "Thread " << thread_id << " became the leader." << std::endl;} else {std::cout << "Thread " << thread_id << " failed." << std::endl;}
}int main() {constexpr int kNumThreads = 10;std::vector<std::jthread> threads;for (int i = 0; i < kNumThreads; ++i) {threads.emplace_back(try_to_be_leader);}std::cout << "Leader is: " << leader_id.load() << std::endl;return 0;
}
在 Compiler Explorer 上查看.
运行结果
Thread 127785328707264 became the leader.
Thread 127785276278464 failed.
Thread 127785297249984 failed.
Thread 127785307735744 failed.
Thread 127785286764224 failed.
Thread 127785318221504 failed.
Thread 127785339193024 failed.
Thread 127785360164544 failed.
Thread 127785370650304 failed.
Thread 127785349678784 failed.
Leader is: 6834516529000622202
说明:compare_exchange_strong
确保只有一个线程能成功设置 leader_id
,实现了多线程下的安全竞争。
场景 3: 协调执行阶段
示例需求
在多线程编程中,不同线程之间需要协调工作,常常需要使用标志位来进行通信。例如,一个线程在数据准备完毕后需要通知其他线程开始工作。这时,我们可以使用 std::atomic<bool>
来实现高效的标志位。
示例代码
#include <atomic>
#include <iostream>
#include <thread>
#include <vector>int main() {std::atomic<bool> ready{false};std::jthread producer([&]() {std::cout << "Producer: Preparing data...\n";// 模拟数据准备std::this_thread::sleep_for(std::chrono::seconds(2));// 数据准备完毕ready.store(true);// 通知等待的线程ready.notify_one();std::cout << "Producer: Data ready.\n";});std::jthread consumer([&]() {// 等待直到 ready 为 trueready.wait(false);std::cout << "Consumer: Data received.\n";});return 0;
}
在 Compiler Explorer 上查看.
说明:通过 wait
和 notify_all
,可以高效地实现生产者-消费者模式,避免频繁轮询带来的性能浪费。
高级篇:内存序模型
std::atomic
支持以下内存序模型:
memory_order_relaxed
:不保证顺序,性能最高。memory_order_acquire
:保证后续读取可见之前的写入。memory_order_release
:保证之前的写入对其他线程可见。memory_order_acq_rel
:结合获取和释放语义。memory_order_seq_cst
:全局顺序一致性,最强保证。
应用场景
- 高性能优化:计数器的无序递增使用
memory_order_relaxed
。 - 同步机制:锁实现中使用
memory_order_acquire/release
。 - 复杂算法:无锁队列、信号处理中平衡性能与一致性。
总结
通过合理使用 std::atomic
,可以在性能与一致性之间找到最佳平衡。尽管其适用范围主要限于简单变量的同步,但其高效、灵活的特性使其成为现代 C++ 多线程编程的重要工具。希望本文的深入探讨能够帮助你更好地掌握并应用 std::atomic
,提升项目的性能与安全性。