STATUS: NOMINAL LOCAL TIME: 00:00:00 返回园内

[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:

  1. 哪些重排序是允许的
  2. 哪些重排序是禁止的
  3. 何时需要确保内存操作的可见性
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的函数

  1. atomic_storeatomic_store_explicit
    设置值,内存序默认为seq_cst,explicit版本可以显示指定内存序
  2. atomic_loadatomic_load_explicit
    读取值
  3. 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。

posted @ 2026-03-15 10:31  猫爹爱猫娘  阅读(2)  评论(0)    收藏  举报