C++多线程——原子变量
转自 编程元年
-
上一节介绍了c++多线程的几种锁,c++多线程的锁的用途是主要用途之一是在多线程编程时避免数据竞争,保证数据的一致性。但是我们知道锁操作是有性能开销的,这也是本节介绍原子操作的原因。(特别说明:尽管原子操作有诸多优势,但在面对更复杂的同步需求,如涉及多个操作的原子性保证或需要进行更复杂的同步逻辑时,锁的使用可能更为合适。此外,原子操作并不总是能提供像锁那样的互斥效果,特别是在需要进行一系列操作时。因此,在选择使用原子操作还是锁时,需要根据具体的应用场景和性能要求来决定。)
-
下面先介绍下原子的基本操作,然后用例子介绍下原子操作和用锁的性能对比。
基本的原子操作
加载(Load)和存储(Store)
- 加载:以原子方式从原子变量中读取数据,并保证读取操作的原子性和可见性。
T value = myAtomicVariable.load(std::memory_order_seq_cst);
- 存储:以原子方式将数据写入到原子变量中,并确保写操作的原子性和对其他线程的可见性。
myAtomicVariable.store(newValue, std::memory_order_release);
交换
- 原子地将原子变量的值替换为新值,并返回旧值。
T oldValue = myAtomicVariable.exchange(newValue, std::memory_order_acq_rel);
比较交换
- compare_exchange_weak/compare_exchange_strong: 尝试将原子变量的值与预期值比较,如果相同则替换为新值,并返回
true;如果不相同,则不做替换并返回false。compare_exchange_weak在失败时不保证原子性,可能需要多次尝试;compare_exchange_strong则保证每次操作的原子性。
bool success = myAtomicVariable.compare_exchange_weak(expectedValue, newValue);
增量和减量操作
- fetch_add/fetch_sub: 原子地增加或减少原子变量的值,并返回操作前的值。
T oldValue = myAtomicFlag.fetch_add(1); // 增加
T oldValue = myAtomicFlag.fetch_sub(1); // 减少
位操作
- 虽然不是所有实现都支持,但某些库可能提供原子的位操作,如
fetch_or,fetch_and,fetch_xor,用于原子地对原子变量进行按位或、按位与、按位异或操作。
内存序
-
上述操作中的
std::memory_order_*参数指定了内存序,控制了操作的可见性和顺序性。常见的内存序有:-
std::memory_order_seq_cst: 顺序一致性顺序,是最严格的内存序,保证了操作的顺序性以及全局的顺序一致性。 -
std::memory_order_acquire/release: 用于实现锁的获取和释放语义,确保对原子变量的读写操作与其他内存操作之间的正确排序。 -
std::memory_order_relaxed: 放宽内存序,不提供任何同步保证,仅保证操作本身的原子性,适用于不需要同步的场合,以获得最佳性能。
-
-
使用这些基本的原子操作,开发者可以在多线程环境中安全地进行读写,避免数据竞争和竞态条件,同时在某些场景下相比传统锁机制提供更好的性能。
比较相同场景多线程使用原子操作和使用锁性能对比
- 为了演示在相同 的场景下使用原子操作与使用锁(如互斥锁
std::mutex)在性能上的对比,我们可以设计一个简单的计数器示例。这个示例将创建多个线程,每个线程都会增加一个共享变量的值,并比较使用原子操作和互斥锁两种方式的执行效率。
使用原子操作
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>
#include <chrono>
std::atomic<int> atomicCounter(0);
void incrementAtomic(int numIterations) {
for (int i = 0; i < numIterations; ++i) {
++atomicCounter;
}
}
int main() {
const int numIterations = 1000000;
const int numThreads = 10;
std::vector<std::thread> threads;
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < numThreads; ++i) {
threads.emplace_back(incrementAtomic, numIterations);
}
for (auto& t : threads) {
t.join();
}
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> duration = end - start;
std::cout << "Atomic: " << atomicCounter << " in " << duration.count() << " seconds" << std::endl;
return 0;
}
使用互斥锁
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
#include <chrono>
std::mutex mutex;
int counter = 0;
void incrementMutex(int numIterations) {
for (int i = 0; i < numIterations; ++i) {
std::lock_guard<std::mutex> lock(mutex);
++counter;
}
}
int main() {
const int numIterations = 1000000;
const int numThreads = 10;
std::vector<std::thread> threads;
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < numThreads; ++i) {
threads.emplace_back(incrementMutex, numIterations);
}
for (auto& t : threads) {
t.join();
}
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> duration = end - start;
std::cout << "Mutex: " << counter << " in " << duration.count() << " seconds" << std::endl;
return 0;
}
-
在这两个示例中,每个示例都会创建10个线程,每个线程对一个共享变量进行1百万次递增操作。第一个示例使用了
std::atomic<int>来保证操作的原子性,第二个示例则使用了std::mutex来保护对共享变量的访问。 -
执行这两个示例并比较输出的时间,通常你会发现使用原子操作的版本执行得更快,特别是在简单读写操作上,因为原子操作避免了线程的阻塞和解锁的开销。但是,实际性能差异可能依赖于具体的硬件、操作系统、编译器优化以及运行时负载等因素。在更复杂的同步需求下,锁的灵活性和控制能力可能带来不同的性能权衡。