在这里插入图片描述

📃个人主页:island1314

⛺️ 欢迎关注:👍点赞 👂🏽留言 😍收藏 💞 💞 💞

  • 生活总是不会一帆风顺,前进的道路也不会永远一马平川,如何面对挫折影响人生走向 – 《人民日报》


atomic

1. 基本概述

1. 核心概念:原子性

  • 原子性: 指一个操作是不可中断的。对于多线程来说,一个原子操作要么完全执行成功,要么完全不执行,中间状态对其他线程是不可见的。
  • std::atomic 作用: 确保对共享变量的读、写和修改操作是原子的,从而避免了对简单类型(如计数器)进行互斥锁保护的需要。

2. 与 std::mutex 的区别

特性 std::atomic std::mutex
开销 。通常通过硬件指令实现,无需内核上下文切换。 。涉及系统调用和线程阻塞/唤醒。
保护范围 仅保护单个数据项的简单操作(读/写/增/减)。 保护一段代码块(临界区),可以包含复杂逻辑和多个数据项。
使用场景 线程安全的计数器、标志、布尔值等。 访问复杂数据结构(如 std::vectorstd::map)或需要多步操作时。

总结: 如果你只是需要安全地对一个整数或布尔值进行简单的增减或读写,std::atomic 提供了比 std::mutex 更高效的解决方案(被称为 无锁编程lock-free)。


2. 用法

std::atomic 是一个类模板,你可以用它来包装几乎所有可平凡复制 (Trivially Copyable) 的类型,但最常用的是整型和布尔型。

1. 基本使用

可以将 std::atomic<T> 视为普通类型 T 的线程安全版本。

#include <atomic>
#include <thread>
#include <iostream>

// 定义一个线程安全的计数器
std::atomic<int> counter{0}; // 初始化为 0

void incrementer() {
    for (int i = 0; i < 10000; ++i) {
        // 对 std::atomic<int> 的自增操作是原子操作
        // 相当于执行了原子的:读 -> 修改 -> 写回
        counter++; 
    }
}

int main() {
    std::thread t1(incrementer);
    std::thread t2(incrementer);

    t1.join();
    t2.join();

    // 最终结果一定是 20000,不会发生数据竞争
    std::cout << "Final counter value: " << counter << "\n"; 
    
    // 基本操作:
    counter = 5; // 原子赋值 (store)
    int val = counter; // 原子读取 (load)
    
    return 0;
}

2. 关键的原子成员函数

std::atomic 提供了一系列用于复杂原子操作的函数:

函数 作用 描述
load() 原子读取 获取当前原子变量的值。
store(T desired) 原子写入 将指定值写入原子变量。
exchange(T desired) 原子交换 将指定值写入原子变量,并返回操作前的值。
fetch_add(T arg) 原子加法 原子性地将 arg 加到当前值上,并返回操作前的值。
fetch_sub(T arg) 原子减法 原子性地从当前值减去 arg,并返回操作前的值。
compare_exchange_weak/strong(T& expected, T desired) CAS 操作 比较并交换 (Compare And Swap)。如果原子变量的当前值等于 expected,则将其设置为 desired,返回 true;否则,将当前值加载到 expected 中,返回 false

compare_exchange (CAS): 这是实现复杂无锁数据结构(如无锁队列)的基石。它允许你实现一个循环,不断尝试修改一个值,直到成功为止。

注意:compare_exchange_weak在某些平台上,即使原子变量的值等于expected,也可能“虚假地”失败(即返回 false)。

  • 这种失败是由于底层硬件或编译器优化导致的,但不会改变原子变量的。
  • compare_exchange_strong 保证在原子变量的值等于 expected 时不会虚假地失败。只要原子变量的值等于 expected,操作就会成功;但compare_exchange_weak 在某些平台上可能比 compare_exchange_strong 更快。
  • compare_exchange_weak 可能会虚假的失败主要是由于硬件层间的缓存一致性和编译器优化等等,compare_exchange strong 要避免这些原因就要付出一定的代价,比如要使用硬件的缓存一致性协议(如 MESI协议)。

示例一:原子变量 vs 非原子变量

#include <iostream>
#include <atomic>
#include <thread>
#include <vector>   

using namespace std;

int cnt;
atomic<int> acnt;

// load 和 store 可以原子的读取和修改atomic封装存储的T对象
void Add1(atomic<int>& cnt){
	int old = cnt.load();
	// 如果cnt的值跟old相等,则将cnt的值设置为old+1,并且返回true,这组操作是原⼦的。 
	// 那么如果在load和compare_exchange_weak操作之间cnt对象被其他线程改了 
	// 则old和cnt不相等,则将old的值改为cnt的值,并且返回false。 
	while (!atomic_compare_exchange_weak(&cnt, &old, old + 1));
	//while (!cnt.compare_exchange_weak(old, old + 1));
}

void f(){
	for (int n = 0; n < 100000; ++n){
		++acnt;
		// Add1的⽤CAS模拟atomic的operator++的原⼦操作 
		// Add1(acnt);
		++cnt;
	}
}


int main() {
	std::vector<thread> pool;
	for (int i = 0; i < 4; ++i) pool.emplace_back(f);
	for (auto& e : pool) e.join();
	cout << "原子计数器为 " << acnt << '\n'
		<< "非原子计数器为 " << cnt << '\n';
	return 0;
}

示例二:原子链表

struct Node {
	int v;
	Node* next;
};
std::atomic<Node*> head(nullptr);

void append(int v, int n) {
	for (int i = 0; i < n; i+=3) {
		Node* oldHead = head;
		Node* newHead = new Node{ v + i, oldHead };
		while (!head.compare_exchange_weak(oldHead, newHead)) {
			newHead->next = oldHead;
		}
	}
}

int main(){
	std::vector<std::thread> threads;
	threads.emplace_back(append, 0, 10);
	threads.emplace_back(append, 20, 10);
	threads.emplace_back(append, 30, 10);
	for (auto& th : threads) th.join();

	for (Node* it = head; it != nullptr; it = it->next)
		std::cout << ' ' << it->v;
	std::cout << std::endl;
	
	Node* it;
	while (it = head){
		head = it->next;
		delete it;
	}
	return 0;
}

// 39 36 29 33 30 26 23 20 9 6 3 0

3. 内存顺序(Memory Ordering)

在 C++11标准库中,std::atomic 提供了多种内存顺序(memory_order)选项,用于控制原子操作的内存同步行为。这些内存顺序选项允许开发者在性能与正确性之间进行权衡,决定了编译器和 CPU 如何对指令进行重排序,特别是在多线程编程中。

通过给原子操作传入 std::memory_order 枚举,可以调整原子操作的性能和同步强度。

内存顺序 作用 描述
memory_order_seq_cst 顺序一致性(默认) 最强保证,确保所有线程看到的操作顺序都一致。如果不理解内存序,就使用它
memory_order_acq_rel 获取 & 释放 适用于读-修改-写操作(如fetch_add或compare_exchange_strong),用于需要 同时实现“获取”和“释放”语义的操作
memory_order_release 释放 阻止它 前面的 读/写操作被重排到它后面。通常用于锁的释放
memory_order_acquire 获取 阻止它 后面的 读/写操作被重排到它前面。通常用于锁的获取
memory_order_consume 限制较弱内存顺序 仅保证依赖于当前加载操作的数据的可见性,通常用于数据依赖的场景。
memory_order_relaxed 宽松 最弱保证。只保证操作本身的原子性,不施加任何顺序约束。性能最高,但几乎不能用于同步。

示例

// 1. 最宽松
std::atomic<int> x(0);
x.store(42, std::memory_order_relaxed); // 仅保证原⼦性

// 2. consume
std::atomic<int*> ptr(nullptr);
int* p = ptr.load(std::memory_order_consume);
if (p) {
 int value = *p; // 保证 p 指向的数据是可⻅的 
}

// 3 4 acquire&release
std::atomic<bool> flag(false);
int data = 0;
// 线程 1 
data = 42;
flag.store(true, std::memory_order_release); // 保证 data = 42 在 flag = true 之前可⻅ 
// 线程 2 
while (!flag.load(std::memory_order_acquire)) {}
std::cout << data; // 保证看到 data = 42 

// 5. acq_rel
std::atomic<int> x(0);
x.fetch_add(1, std::memory_order_acq_rel); // 保证前后的操作不会被重排序


// 6. seq_cst
std::atomic<int> x(0);
x.store(42, std::memory_order_seq_cst); // 全局顺序⼀致性 
int value = x.load(std::memory_order_seq_cst);

警告: 错误地使用弱内存序(如 acquirerelaxed)会破坏程序的逻辑,导致数据竞争或同步失败。只有当你对内存模型有深入理解时才应该使用非默认的内存序。

内存顺序的关系,宽松到严格:memory_order_relaxed < memory_order_consume < memory_order_acquire < memory_order_release < memory_order_acq_rel < memory_order_seq_cst

  • 宽松的内存顺序(如 memory_order_relaxed)性能最好,但同步语义最弱。
  • 严格的内存顺序(如 memory_order_seq_cst)性能最差,但同步语义最强
自旋锁

概念:自旋锁(spinlock)是最朴素的“锁”:

  • 拿不到锁时线程不睡眠,而是一直循环检测,直到锁被释放。这种方式可以减少 上下文切换 的开销,但在锁竞争激烈或锁持有时间较长的时候,会导致 CPU 资源的浪费
  • 适合 “预计等待时间极短” 的场景,例如线程在几条指令后就会释放锁;否则空转会白白烧 CPU。

atomic_flag

  • 与所有atomic的特化不同,它保证是免锁的。
  • atomic<bool> 不同,atomic flag不提供加载或存储操作。
  • 主要提供 test_and_set 操作将flag原子的设置为true并返回之前的值,clear原子将flag设置为false。

下面一个样例演示了用 atomic_flag 实现 自旋锁

class SpinLock {
private:
	std::atomic_flag flag = ATOMIC_FLAG_INIT; // 默认初始化为 false
public:
	void lock() {
		// test_add_set 把内部值设为 true, 并返回之前值
		// 如: 第一个进来的线程将值原子的设为true,返回false;后面进来的线程将原子的值设为true,返回true,所以卡在这里空转, 
		// 直到第一个进去的线程unlock,将值设为false
		while (flag.test_and_set(std::memory_order_acquire));
	}
	void unlock() {
		flag.clear(std::memory_order_release);
	}
};

// 测试自旋锁
void worker(SpinLock& lock, int& shareValue) {
	lock.lock();
	for (int i = 0; i < 100000; i++) {
		++shareValue;
	}
	lock.unlock();
}

int main() {
	SpinLock lock;
	int shareValue = 0;
	std::vector<std::thread> threads;
	for (int i = 0; i < 4; ++i) {
		threads.emplace_back(worker, std::ref(lock), std::ref(shareValue));
	}

	// 等待所有线程完成
	for (auto& t : threads) t.join();

	std::cout << "Final Shared Value: " << shareValue << std::endl;

	return 0;
}

★,°:.☆( ̄▽ ̄)/$:.°★ 】那么本篇到此就结束啦,如果有不懂 和 发现问题的小伙伴可以在评论区说出来哦,同时我还会继续更新关于【C++现代】的内容,请持续关注我 !!

在这里插入图片描述

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐