C++笔记---并发支持库(atomic)

1. atomic

C++11 引入的 <atomic> 头文件和 std::atomic 模板是无锁并发编程的核心,用于实现多线程间的原子操作,避免数据竞争(data race),替代传统的互斥锁(如 std::mutex)以提升并发性能。

使用atomic的成员函数,对T类型的数据进行操作是原子的,这就使得某些简单的操作不再需要我们去加锁访问,而是直接采用原子操作。例如,一个多线程共享的计数器:

cpp 复制代码
atomic<int> a_cnt = 0;
int cnt = 0;

void func()
{
	for (int i = 0; i < 100000; i++)
	{
		++a_cnt;
		++cnt;
	}
}

int main()
{
	vector<thread> pool;
	for (int i = 0; i < 4; i++)
	{
		pool.emplace_back(func);
	}
	for (auto& t : pool)
	{
		t.join();
	}
	cout << "原子:" << a_cnt << endl;
	cout << "非原子:" << cnt << endl;
	return 0;
}

值得注意的是,atomic作为模板,实际上并不完全支持所有类型。

主要支持的是整型家族、指针类型,以及任何满足 CopyConstructible 和 CopyAssignable 的 **可简单复制 (TriviallyCopyable)**类型,例如:

cpp 复制代码
struct Counters { int a; int b; }; // user-defined trivially-copyable type
std::atomic<Counters> cnt;         // specialization for the user-defined type

从C++20开始,atomic对智能指针进行了特化:

如果如下六个函数的返回值均为true ,则说明类型 T 可以使用原子操作,否则不行:

cpp 复制代码
std::is_trivially_copyable<T>::value
std::is_copy_constructible<T>::value
std::is_move_constructible<T>::value
std::is_copy_assignable<T>::value
std::is_move_assignable<T>::value
std::is_same<T, typename std::remove_cv<T>::type>::value

注意 :std::atomic 对象不可拷贝、不可移动 ,因为拷贝 / 移动会破坏原子性。

1.1 核心成员函数

|----------------------------------|---------------------------------------------------|
| 函数 | 功能 |
| load | 原子读取值 |
| store | 原子写入值 |
| exchange | 原子交换值(返回旧值,写入新值) |
| compare_exchange_weak/strong | 比较并交换(CAS):核心原子操作,实现无锁算法的基础 |
| fetch_add/fetch_sub | 原子加减(返回旧值) ,仅对整数 / 指针类型有效 |
| operator++/-- | 原子自增 / 自减(重载运算符,等价于fetch_add(1)/fetch_sub(1)) |
| operator= | 原子赋值(等价于 store(val)) |
| is_lock_free() | 判断当前原子操作是否 "无锁"(否则内部可能用互斥锁实现) |

1.2 CAS操作

CAS 是无锁编程的基石 ,即 Compare And Set 或 Compare And Swap,上面所有对值进行修改的成员函数,底层都是通过如下两个函数实现:

cpp 复制代码
bool compare_exchange_weak( T& expected, T desired)
bool compare_exchange_strong( T& expected, T desired)

这两个函数均为原子操作,依赖于硬件提供的CAS指令,核心原理为:

比较原子对象的当前值 与 expected:

  • 若相等 :将原子对象值设为 desired,返回 true;
  • 若不等 :将 expected 更新为原子对象的当前值,返回 false。

weak 与 strong 的区别 在于是否使用缓存一致性协议:

  • weak:弱版本,可能 "伪失败"(值相等但返回 false),性能更高;
  • strong:强版本,值相等时必成功,无伪失败。

例如,operator++的底层实现可能与Add函数的实现相似:

cpp 复制代码
atomic<int> a_cnt = 0;
int cnt = 0;

void Add(atomic<int>& cnt)
{
	int old = cnt.load();
	// cnt与old的值相同,则将new赋值给cnt,否则将cnt的值更新给old
	// 确保将数据写回之前,没有其他线程对目标数据进行了修改,进而导致数据的覆盖
	// 本质上来说,atomic的原理就是在将数据写回之前验证数据是否已被其他线程修改
	// 若已被修改则重新计算更新后的值,并再次尝试写回,直到某次成功
	// while (!atomic_compare_exchange_weak(&cnt, &old, old + 1));
	while (!a_cnt.compare_exchange_weak(old, old + 1));
}

void func()
{
	for (int i = 0; i < 100000; i++)
	{
		Add(a_cnt);
		++cnt;
	}
}

int main()
{
	vector<thread> pool;
	for (int i = 0; i < 4; i++)
	{
		pool.emplace_back(func);
	}
	for (auto& t : pool)
	{
		t.join();
	}
	cout << "原子:" << a_cnt << endl;
	cout << "非原子:" << cnt << endl;
	return 0;
}

再例如,使用CAS操作实现无锁的链式栈(部分代码):

cpp 复制代码
#pragma once
#include <atomic>

template <typename T>
class Node
{
	int _val;
	Node* _next;
	Node(int val = 0, Node* next = nullptr)
		:_val(val)
		,_next(next)
	{ }
};

template <typename T>
class LockFreeStack
{
public:
	void push(const T& val)
	{
		Node<T>* newNode = new Node<T>(val, _head.load());
		while (!_head.compare_exchange_weak(newNode->next, newNode));
	}
private:
	std::atomic<Node<T>*> _head = nullptr;
};

1.3 内存序(Memory Order)

std::atomic 的所有操作都可指定内存序参数(默认 std::memory_order_seq_cst),用于控制:

  • 指令重排序:编译器 / CPU 是否会重排原子操作的前后指令;
  • 内存可见性:一个线程的写操作对另一个线程的读操作的可见性。

例如:

|--------------------------|--------------------------------------------|
| 内存序枚举值 | 含义 |
| memory_order_relaxed | 松散序 :仅保证操作本身原子性,无可见性 / 重排序约束(最弱) |
| memory_order_consume | 消费序 :保证对依赖于该原子操作的读写不重排(C++20 已弃用) |
| memory_order_acquire | 获取序 :读操作,禁止后续指令重排到该操作前,且能看到之前的释放操作 |
| memory_order_release | 释放序 :写操作,禁止之前指令重排到该操作后,且写结果对获取序可见 |
| memory_order_acq_rel | 同时具备 acquire 和 release 语义(用于读写操作,如 CAS) |
| memory_order_seq_cst | 顺序一致序 :所有线程看到的操作顺序一致(最强,默认,性能最差) |

通常来说使用默认内存序即可,各内存序的效率差别实际上并不大,优先保证正确性。在要求极致性能的场景下,我们再考虑对内存序进行优化。

2. 原子操作实现自旋锁

cpp 复制代码
#pragma once
#include <atomic>

class SpinLock1
{
public:
	void lock()
	{
        // exchange: 将对象值设置为参数值,返回原本的值
		while (_flag.exchange(true))
		{
			// 自旋等待锁释放
		}
	}
	void unlock()
	{
		_flag.store(false);
	}
private:
	std::atomic<bool> _flag = false;
};

class SpinLock2
{
public:
	void lock()
	{
		while (_flag.test_and_set())
		{
			// 自旋等待锁释放
		}
	}
	void unlock()
	{
		_flag.clear();
	}
private:
	std::atomic_flag _flag = ATOMIC_FLAG_INIT;
};

3. 无锁队列

未来补充。。。

相关推荐
captain3761 分钟前
Spring IoC和DI(2)
java·后端·spring
Shaoshing4 分钟前
高可用与核心概念
java·mq
ACP广源盛1392462567319 分钟前
HDMI2.1 同源双屏方案选型,GSV6502@ACP 工程评估笔记
笔记·硬件架构·音视频·硬件工程·hdmi·国产芯片
wuyoutrans26 分钟前
冰岛专利审查意见答复阶段,翻译文件多久可以交付,是否包含术语校对服务?
笔记
小羊没烦恼!34 分钟前
深入浅出SQL Server中的死锁
java·开发语言·前端·数据库·后端·spring·c#
白露与泡影35 分钟前
Java 面试八股文及答案整理 | 后端面试高频题(2026)
java·开发语言·面试
\光辉岁月/42 分钟前
6.数组工具类
java·数组·数组工具类
_橙时_43 分钟前
【MOEA/D的学习小笔记】
笔记·学习·多目标优化·moea/d
布莱克6051 小时前
QtWidgets 与 QML:两大 UI 开发框架的全面对比
开发语言·c++·qt·ui·架构
hai_android1 小时前
Android 设备锁定:Kiosk Mode 实战
android·java·android jetpack