[C++][多线程]原子操作
[C++]原子操作
C++11引入了std::atomic,为并发编程提供了原子操作。
原子操作是不可分割的操作,要么完全执行,要么完全不执行,不会被其他线程中断。在多线程环境中,原子操作保证了数据的一致性,避免了竞态条件。
与锁的比较
既然老版本的C++提供了锁来解决数据竞争和同步问题,为何还要使用原子操作呢?其实是对性能的考虑。我们都学习过,锁的主要开销在于锁竞争激烈时发生的系统调用和上下文切换;而原子操作具有·无上下文切换、防止编译器重排序的特点。接下来通过代码对比一下:
#include <iostream>
#include <mutex>
#include <thread>
#include <chrono>
#include <atomic>
const int num_threads = 5;
const int increments_per_thread = 1000000;
std::mutex mtx;
int main() {
int counter = 0;
auto start = std::chrono::high_resolution_clock::now();
std::vector<std::thread> threads;
for (int i = 0; i < num_threads; ++i) {
threads.emplace_back([&]() {
for (int j = 0; j < increments_per_thread; ++j) {
counter++; // 不加锁
}
});
}
for (auto& t : threads)
t.join();
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
std::cout << "No lock final counter value: " << counter << std::endl;
std::cout << "Time taken: " << duration << " ms" << std::endl;
threads.clear();
counter = 0;
start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < num_threads; ++i) {
threads.emplace_back([&]() {
for (int j = 0; j < increments_per_thread; ++j) {
mtx.lock();
counter++;
mtx.unlock();
}
});
}
for (auto& t : threads)
t.join();
end = std::chrono::high_resolution_clock::now();
duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
std::cout << "Lock final counter value: " << counter << std::endl;
std::cout << "Time taken: " << duration << " ms" << std::endl;
threads.clear();
std::atomic<int> atomic_counter(0);
start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < num_threads; ++i) {
threads.emplace_back([&]() {
for (int j = 0; j < increments_per_thread; ++j) {
atomic_counter.fetch_add(1, std::memory_order_relaxed); // 宽松序,允许重排
}
});
}
for (auto& t : threads)
t.join();
end = std::chrono::high_resolution_clock::now();
duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
std::cout << "Atomic final counter value: " << atomic_counter.load() << std::endl;
std::cout << "Time taken: " << duration << " ms" << std::endl;
}
最后输出结果
No lock final counter value: 1367279
Time taken: 24 ms
Lock final counter value: 5000000
Time taken: 408 ms
Atomic final counter value: 5000000
Time taken: 174 ms
可以发现原子操作运行时间更短。
内存序
前面提到原子操作可以防止编译器重排序,这里引入内存序的概念。
内存序用来控制多线程程序中内存操作的可见性顺序。它告诉编译器和CPU:
- 哪些重排序是允许的
- 哪些重排序是禁止的
- 何时需要确保内存操作的可见性
enum memory_order {
memory_order_relaxed, // 宽松序:只保证原子性,允许重排序
memory_order_consume, // 消费序:较少使用,类似acquire但更弱
memory_order_acquire, // 获取序:读操作的同步点,防止后续操作重排到前面
memory_order_release, // 释放序:写操作的同步点,防止前面操作重排到后面
memory_order_acq_rel, // 获取-释放序:同时具有acquire和release语义
memory_order_seq_cst // 顺序一致性:最强的内存序,全局统一顺序
};
而提供六种不同的内存序,一方面是对性能的要求不同,另一方面是对重排序的接受程度不同。
const int num_threads = 5;
const int increments_per_thread = 1000000;
std::atomic<int> atomic_counter(0);
int main() {
std::vector<std::thread> threads;
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < num_threads; ++i) {
threads.emplace_back([&]() {
for (int j = 0; j < increments_per_thread; ++j) {
atomic_counter.fetch_add(1, std::memory_order_relaxed); // 宽松序,允许重排
}
});
}
for (auto& t : threads)
t.join();
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
std::cout << "Relaxed time taken: " << duration << " ms" << std::endl;
threads.clear();
atomic_counter.store(0); // 重置计数器
start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < num_threads; ++i) {
threads.emplace_back([&]() {
for (int j = 0; j < increments_per_thread; ++j) {
atomic_counter.fetch_add(1, std::memory_order_seq_cst); // 顺序一致性,禁止重排
}
});
}
for (auto& t : threads)
t.join();
end = std::chrono::high_resolution_clock::now();
duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
std::cout << "Seq time taken: " << duration << " ms" << std::endl;
}
这段代码输出
Relaxed time taken: 172 ms
Seq time taken: 180 ms
除了性能差别,对重排的要求又决定了不同内存序有不同的应用场景:对于简单的计数器,用memory_order_relaxed即可,而对于生产者-消费者问题,则需要保证编译器不会重排:
void producer() {
data.store(42, std::memory_order_relaxed); // 1. 写入数据
data_ready.store(true, std::memory_order_release); // 2. 发布信号(不能重排到1之前)
}
void consumer() {
// 获取信号(后续读取不能重排到这之前)
while (!data_ready.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
int value = data.load(std::memory_order_relaxed);
std::cout << "consuming: " << value << std::endl;
}
std::atomic的函数
atomic_store和atomic_store_explicit:
设置值,内存序默认为seq_cst,explicit版本可以显示指定内存序atomic_load和atomic_load_explicit:
读取值- compare_exchange:CAS操作
有两个相似的函数:
// strong版本:原子地比较并交换,如果比较失败则不交换
bool atomic_compare_exchange_strong_explicit(
atomic<T>* obj,
T* expected,
T desired,
memory_order success,
memory_order failure
);
// weak版本:原子地比较并交换,可能出现伪失败
bool atomic_compare_exchange_weak_explicit(
atomic<T>* obj,
T* expected,
T desired,
memory_order success,
memory_order failure
);
至于这两个函数的细节与差别,读者去向AI chat提问一下就明白啦。简要来讲,weak版本允许“伪失败”,在循环中性能更好。
4. atomic_fetch_*:
这一系列函数先读取原子变量的当前值,然后对其进行运算,最后将结果写回,整个过程不可中断,并会返回修改前的原始值,也都有explicit版本。
包括add、sub、and、or、xor
原子操作总结
原子操作能够以更高的性能解决互斥锁解决的数据竞争和同步问题,所以可以应用原子操作以减少代码对互斥锁的依赖,实现无锁数据结构,提高并发效率;关键在于设置合适的内存序,已经在循环中使用weak版本的compare&exchange。

浙公网安备 33010602011771号