std::atomic_ref¶
| 标准 | 难度 | 状态 | 分级 | 证据 | 审阅 | 最后校验 |
|---|---|---|---|---|---|---|
| C++20 | 4/5 | final | A | 3 条(manual 3) | 单审(zyuhao7) | 2026-10-26 |
编译器支持:GCC 10+ · Clang 19+ · MSVC 19.28+。版本查询:cppreference 编译器支持。
主张与证据¶
| 主张 | 支持证据 |
|---|---|
| 用特性测试宏检测 std::atomic_ref,不支持时回退到自有实现 | ev-stdexec-atomic-ref-guard-001 |
| 把 mmap 得到的 io_uring 环形缓冲区指针绑定到 atomic_ref | ev-stdexec-atomic-ref-submission-ring-002 |
| 对同一块普通内存按 relaxed 与 acquire 内存序读取共享队列指针 | ev-stdexec-atomic-ref-load-store-003 |
本站证据页:ev-stdexec-atomic-ref-guard-001 · ev-stdexec-atomic-ref-load-store-003 · ev-stdexec-atomic-ref-submission-ring-002
解决什么问题¶
std::atomic<T> 把原子操作做进了类型:要原子访问一个整数,就得把整数声明成 std::atomic<int>。但有些对象不能改类型——内核与用户态共享的 mmap 内存、C 库的结构体、按固定布局序列化的缓冲区、第三方接口传进来的普通指针。这些地方既不能把字段换成 std::atomic,又需要原子读写。
std::atomic_ref<T> 是对已有对象的原子引用。它不改变对象的类型和布局,只在对该对象操作时提供原子语义。代价是正确性责任转移到调用方:对齐、生命周期和“不得并发非原子访问”都需要自己保证。标准提案为 P0019R8。
基础用法¶
构造 atomic_ref 时传入已有对象,之后通过引用执行 load、store、fetch_add、compare_exchange 等操作。
#include <atomic>
#include <iostream>
#include <thread>
#include <vector>
int main() {
unsigned value = 0;
std::atomic_ref<unsigned> ref(value);
std::vector<std::thread> workers;
for (int i = 0; i < 4; ++i) {
workers.emplace_back([&ref] {
for (int j = 0; j < 1000; ++j) {
ref.fetch_add(1, std::memory_order_relaxed);
}
});
}
for (auto& worker : workers) {
worker.join();
}
std::cout << value << ' ' << ref.load(std::memory_order_acquire) << '\n';
ref.store(10, std::memory_order_release);
const unsigned previous = ref.exchange(20);
std::cout << previous << ' ' << value << ' ' << ref.load() << '\n';
}
输出 4000 4000 与 10 20 20。四个线程对同一个普通变量 value 各做 1000 次原子累加,随后用 store/exchange 展示发布写入和读改写;普通变量本身仍是 unsigned,原子语义只通过 ref 生效。完整版本见 examples/cpp20/atomic_ref.cpp。
T 必须是可平凡复制的类型;被引用对象必须满足 std::atomic_ref<T>::required_alignment,并处于可写内存中。atomic_ref 只持有地址,不拥有对象,因此所有引用的生命周期都必须落在对象生命周期之内。
进阶用法¶
内存序与 std::atomic 一致:默认 memory_order_seq_cst,需要放松时显式传 relaxed、acquire、release 或 acq_rel。读改写操作可用 fetch_add、fetch_sub、fetch_or、exchange;比较交换有 compare_exchange_weak 与 compare_exchange_strong。
对共享数据结构,常见做法是每个需要原子访问的普通字段各建一个 atomic_ref,在构造时绑定到固定地址。stdexec 的 io_uring 后端就是如此:提交队列和完成队列的 head/tail 位于 mmap 内存中,用 atomic_ref<__u32> 绑定后,提交线程用 release 发布 tail,读取方按 acquire 观察;内核更新的 head 只做 relaxed 读取,因为同步由环形缓冲区的生产者/消费者协议保证。
atomic_ref 可以拷贝,拷贝出的引用指向同一个对象。多个 atomic_ref 之间的原子操作彼此原子;但只要还有 atomic_ref 引用某个对象,该对象就必须只通过这些 atomic_ref 访问,不能再同时走普通读写或 std::atomic。也不能让另一个 atomic_ref 并发引用它的子对象。
真实项目证据¶
| 项目 | 位置 | 用法 | 为什么典型 |
|---|---|---|---|
| stdexec | include/stdexec/__detail/__atomic.hpp#L68-L69 |
#if __cpp_lib_atomic_ref >= 201806L 后直接 using std::atomic_ref |
跨工具链使用前的特性检测与回退 |
| stdexec | include/exec/linux/io_uring_context.hpp#L217-L226 |
用 mmap 区域中的 sq_off.head/sq_off.tail 构造 atomic_ref<__u32> |
内核共享内存布局不能改类型,只能原地加原子语义 |
| stdexec | include/exec/linux/io_uring_context.hpp#L239-L240 |
对 tail 做 relaxed 读、对 head 做 acquire 读 |
生产者/消费者协议需要逐字段选择内存序 |
三条证据均为 manual 级:作者复核,尚未引入第二名审核者。第一条是生产头文件的兼容层,后两条来自 Linux io_uring 执行器;同一文件第 276 行用 release 存储发布 tail,与第 239-240 行构成完整的发布/获取配对。
陷阱与边界¶
并发非原子访问是数据竞争。 atomic_ref 只让经过它的访问具有原子性。atomic_ref 存在期间,对象必须只通过 atomic_ref 访问;另一个线程直接读写、或改用 std::atomic,都破坏这条排他约定。也不要让另一个 atomic_ref 并发引用同一对象的子对象。
对齐要求可能比 T 本身更严格。 对象必须对齐到 std::atomic_ref<T>::required_alignment。从外部缓冲区取地址时,用 alignas(std::atomic_ref<T>) 或运行时检查对齐,不能假设 T 的自然对齐已经足够。
生命周期由调用方保证。 atomic_ref 不延长对象寿命。对已经释放的对象、栈上已经离开作用域的变量做原子操作,都是悬空访问。
不保证无锁。 用 is_lock_free() 或 is_always_lock_free 检查;某些平台上原子操作可能退化为内部锁,实时性或信号安全场景要单独验证。
不能用于位域和只读存储。 位域没有独立地址;需要写入的对象必须位于可写内存,不能指向字符串字面量或只读段。
共享内存的布局约束仍然存在。 atomic_ref 不改变布局,但要求另一端也按相同的类型、对齐和内存序访问。跨进程或跨内核边界时,这些约定必须写进协议,而不是指望类型系统。
库联动¶
std::atomic与std::memory_order:语义完全一致,atomic_ref只是把同一组操作应用到外部对象上。- io_uring / mmap:环缓冲区的 head/tail 是固定布局的普通整数,
atomic_ref是在不改变 ABI 的前提下加原子性的直接工具。 - 无锁数据结构:节点中由外部分配、布局固定的字段可用
atomic_ref接入无锁算法。 - OS 与设备共享缓冲区:DMA 或共享内存区域通常由驱动定义布局,只能通过引用式原子操作访问。
- 线程检查工具:ThreadSanitizer 等工具对原子操作有专门标注;绕过它们直接读写同一对象会留下无法解释的竞争报告。
横向对比¶
| 方案 | 改变对象类型 | 可用于固定布局内存 | 生命周期责任 | 主要代价 |
|---|---|---|---|---|
std::atomic<T> |
是 | 否 | 对象自身 | 无法接入外部定义的布局 |
std::atomic_ref<T> |
否 | 是 | 调用方 | 对齐、生命周期与并发访问约束 |
| 互斥量 | 否 | 需要额外锁对象 | 调用方 | 阻塞、需要共享锁,不能用于信号或中断上下文 |
GCC __atomic 内建 |
否 | 是 | 调用方 | 不可移植,需手写内存序与类型处理 |
跨标准演进:C++20 通过 P0019R8 引入 std::atomic_ref;它填补了“对象布局不能改,但仍需原子访问”的场景,而不是替代 std::atomic。