news 2026/5/5 21:53:02

CppCon 2024 学习:Interesting Upcoming Features from Low Latency, Parallelism and Concurrency

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CppCon 2024 学习:Interesting Upcoming Features from Low Latency, Parallelism and Concurrency

一、背景:为什么需要 Atomic Min / Max(P0493)

1⃣ 问题本质

在多线程程序中,经常会出现这样的需求:

多个线程并发地更新一个共享变量,用来记录最小值 / 最大值
数学上可以抽象为:
x ← min ⁡ ( x , v i ) 或 x ← max ⁡ ( x , v i ) x \leftarrow \min(x, v_i) \quad \text{或} \quad x \leftarrow \max(x, v_i)xmin(x,vi)xmax(x,vi)
其中:

  • x xx是共享原子变量
  • v i v_ivi是线程i ii提供的候选值

2⃣ 没有原子操作会发生什么?

如果用普通变量或非原子更新:

if(v<x)x=v;

在并发下会出现:

  • 读-改-写非原子
  • 线程交错导致丢更新(lost update)
  • 数据不一致、不可预测行为(UB)

3⃣ 为什么“加锁”不够好?

虽然mutex能解决正确性问题,但:

  • 高争用时性能差
  • 阻塞 → 上下文切换
  • 难以构建lock-free/wait-free结构

4⃣ Atomic Min / Max 的核心价值

把 “比较 + 更新” 合并成一个原子操作
保证:
update ( x ) ; 是线性化的 \text{update}(x) ;\text{是线性化的}update(x);是线性化的
从而实现:

  • 无锁并发更新
  • 数据完整性
  • 高并发可扩展性

二、典型应用场景

Atomic min / max 在以下领域非常常见:

Lock-free 数据结构

  • 并发优先级队列
  • 最小时间戳 / 最大序号维护

并行归约(Parallel Reduction)

  • OpenMP / TBB / GPU-style reduction
  • 例如:并行计算最小误差、最大评分

优化算法

  • 全局最优解(best-so-far)
  • 分支限界(branch & bound)

统计信息收集

  • 延迟最大值
  • 最小响应时间
  • 峰值资源占用

三、C++26 提议接口(标准层面)

1⃣ 非成员函数形式

namespacestd{template<classT>Tatomic_fetch_max(atomic<T>*obj,typenameatomic<T>::value_type arg)noexcept;template<classT>Tatomic_fetch_max_explicit(atomic<T>*obj,typenameatomic<T>::value_type arg,memory_order order)noexcept;// 同样提供 atomic_fetch_min / _explicit}

2⃣ 返回值语义(非常重要)

atomic_fetch_add一致:

返回的是“更新前的旧值”
形式化表达:
old = x before \text{old} = x_{\text{before}}old=xbefore
并执行:
x ← max ⁡ ( x , arg ) x \leftarrow \max(x, \text{arg})xmax(x,arg)

3⃣ 成员函数版本(atomic)

标准还会提供:

std::atomic<T>a;autoold=a.fetch_max(v);autoold=a.fetch_max(v,std::memory_order_relaxed);

四、内存序(memory_order)语义

默认版本

atomic_fetch_max(&a,v);

等价于:

atomic_fetch_max_explicit(&a,v,std::memory_order_seq_cst);

显式版本的使用原则

  • 统计 / reduction
    memory_order_relaxed
  • 同步 / 发布语义
    release/acquire
    数学上可以理解为:
    atomicity ; ≠ ; ordering \text{atomicity} ;\neq; \text{ordering}atomicity;=;ordering
    Atomic min/max 只保证原子性,内存顺序仍需你自己选。

五、实现方式:标准并不强制怎么实现

C++26 只规定语义,实现依赖平台。

六、实现方式一:CAS Loop(Compare-And-Swap)

1⃣ 基本 CAS 实现

T old=a.load();while(old<v&&!a.compare_exchange_weak(old,v)){// old 被更新为当前值}returnold;

数学模型

loop until { x ≥ v or CAS succeeds \text{loop until } \begin{cases} x \ge v \\ \text{or CAS succeeds} \end{cases}loop until{xvor CAS succeeds

2⃣ 问题

  • 高争用时:
    • 自旋严重
    • cache line ping-pong
  • 性能随线程数恶化

七、实现方式二:Optimized CAS

优化点包括:

  • load判断是否需要 CAS
  • 减少不必要的 CAS 尝试
  • 使用compare_exchange_weak+ backoff
    这种实现:
  • 低~中等争用下性能可接受
  • 在无硬件指令的平台上是主力方案

八、实现方式三:硬件直接支持(最快)

1⃣ Direct Instruction

部分架构提供类似指令:

  • x86:LOCK CMPXCHG组合优化
  • GPU:原生 atomic min/max
  • RISC-V / ARM:特定原子扩展
    效果:
  • 单条或少量指令
  • 极低延迟
  • 极高吞吐

2⃣ LL/SC(Load-Link / Store-Conditional)

在 ARM / PowerPC 上:

LL r0, [x] cmp r0, v SC r1, v, [x]

若中途被打断,SC失败自动重试。

九、性能对比总结


实现方式特点性能
基本 CAS简单、可移植高争用差
优化 CAS工程友好可接受
LL/SC架构支持很好
硬件指令原生支持最优

十、为什么 C++26 才加?

原因总结:

  1. 各架构支持不统一(早期)
  2. API 设计需要与 memory_order 对齐
  3. 避免滥用 / 误用
  4. 与 GPU / 并行算法需求成熟度有关

十一、一句话总结(工程视角)

Atomic min/max 把“并发归约”的核心原语,正式提升为标准一等公民。
公式化总结:
atomic_fetch_max = atomic ( max ⁡ ) \text{atomic\_fetch\_max} = \text{atomic}( \max )atomic_fetch_max=atomic(max)
它不是语法糖,而是:

  • lock-free 结构的地基
  • 并行算法的标配
  • 高性能统计与优化的关键原语

一、这篇论文到底在解决什么问题?(一句话版)

把“并发条件更新(conditional write)”这个长期存在、硬件早已支持的原语,正式引入 C++<atomic>标准库。
用公式表示就是,把下面这个并发模式:
x ← max ⁡ ( x , v ) 或 x ← min ⁡ ( x , v ) x \leftarrow \max(x, v) \quad\text{或}\quad x \leftarrow \min(x, v)xmax(x,v)xmin(x,v)
升级为标准原子读-改-写操作(RMW)

二、历史背景:为什么不是新东西,却拖了这么久?

1⃣ 原子加法比原子 max/min 还“年轻”

  • fetch_add:1982(NYU Ultracomputer)
  • fetch_max / fetch_min:1988 年前后就已经出现
    比很多人想象的要早
    但问题是:
  • 各架构语义不一致
  • C++ 内存模型成熟得比较晚
  • conditional write 在标准里一直是“灰色地带”

2⃣ 为什么应用层强烈需要?

论文列的动机,本质可以归为四类:

Lock-free 数据结构

  • 无锁队列
  • 序号 / 时间戳推进

并行归约(reduction)

  • OpenMP 的reduction(max: ...)
  • 数据并行统计

优化 / 搜索算法

  • global best-so-far
  • branch & bound 剪枝

统计信息

  • 最大延迟
  • 峰值内存
  • 最大输入规模
    这些场景的共同点是:

写入发生不频繁,但读取和比较非常频繁

三、核心难点:conditional write 的语义之争

这是整篇论文最关键、也最有争议的部分。

1⃣ 什么是 conditional write?

伪代码:

if(new>old)old=new;

在并发里,这可以有两种实现语义:

2⃣ 方案 A:read-modify-write(RMW)

无论值是否改变,都算一次“写”
x ← max ⁡ ( x , v ) x \leftarrow \max(x, v)xmax(x,v)
特点:

  • fetch_add完全一致
  • 每个参与线程都要抢 cache line
  • 高争用下性能差

3⃣ 方案 B:read-and-conditional-store(RCS)

只有真的变大 / 变小,才写
if v > x then x ← v \text{if } v > x \text{ then } x \leftarrow vifv>xthenxv
特点:

  • 写入次数显著减少
  • 硬件(ARM / RISC-V / POWER)天然支持
  • 性能非常好
    但问题是:

不是严格意义上的 RMW

4⃣ 标准委员会的最终结论(R3 → R5)

语义上:规定为 RMW
实现上:允许用 RCS + 修补
也就是:

行为像 RMW,但实现可以“偷懒”

四、三种 CAS-loop 实现对比(5.1 / 5.2 / 5.3)

这是论文里工程含量最高的部分。

5.1 非 conforming(不符合标准)的实现

while(max(v,t)!=t){if(pv->compare_exchange_weak(t,v))break;}

特点:

  • 如果值没变 → 完全不写
  • 不满足 RMW 语义
  • 被标准否掉

5.2 纯 RMW 实现(最“老实”的)

while(!pv->compare_exchange_weak(t,max(v,t)));

特点:

  • 每轮都会写
  • cache line 抢占严重
  • 高争用下性能灾难

5.3“聪明”的折中实现(最终推荐)

autot=(mr!=m)?pv->fetch_add(0,m):pv->load(mr);while(max(v,t)!=t){if(pv->compare_exchange_weak(t,v,m,mr))returnt;}returnt;

这个实现的关键思想:

  • 只有当 memory_order 要求 release 时,才强制一次写
  • 否则:
    • 允许只读不写
  • fetch_add(0)人为制造一次“可观察的写”

为什么这是合法的吗?

因为:
额外一次 dummy 写 ; ⇏ ; 可观测语义变化 \text{额外一次 dummy 写} ;\not\Rightarrow; \text{可观测语义变化}额外一次dummy;;可观测语义变化
在 C++ 内存模型下,这是不可区分的行为

五、为什么没有 infix operator?

现状:

  • +=→ 有
  • &=→ 有
  • max=→ 不存在

能不能提供“返回新值”的接口?

比如:

autonew=a.fetch_max_new(v);

论文结论是:不值得
原因:

  • max/min不可逆操作
  • 新值可以用:
    max ⁡ ( old , v ) \max(\text{old}, v)max(old,v)
    即:
autoold=a.fetch_max(v);autonewv=std::max(old,v);

所以最终只保留fetch_xxx(返回旧值)

六、指针语义:为什么这么“严格”?

标准规定:

fetch_max/fetch_min对指针的比较
等价于使用std::max / std::min
也就是说:

  • 只能比较:
    • 指向同一个 complete object
    • 或同一数组内的指针
      否则:

<不形成 strict weak ordering
行为未定义

为什么不像其他 atomic 那样“兜底”?

原因是:

  • fetch_add可能产生“新值”
  • fetch_max只会返回:
    • 旧值
    • 或调用者提供的值
      不“创造”新指针
      所以不提供 escape clause

工程建议(论文暗示)

如果你真的需要:

atomic<T*>a;

跨对象比较:
自己转成uintptr_t

七、为什么浮点被移除了?

这是 R5 的重要变化。

根本原因:浮点的 max/min 不是良序

问题点:

  • NaN
  • +0 / -0
  • IEEE 754 特殊规则
    例如:
    max ⁡ ( NaN , x ) ≠ x \max(\text{NaN}, x) \neq xmax(NaN,x)=x

结论

  • std::min / std::max本身就对浮点特殊处理
  • 原子语义会更复杂
  • 已有P3008专门处理:
    本提案专注整数 & 指针

八、Benchmark:为什么硬件支持这么重要?

测试平台

  • ARM Graviton3(64 核)
  • 对比:
    • CAS-loop(smart)
    • 硬件指令ldsmax

结果趋势(简化版)

核数CAS-loop硬件指令
2相近相近
8CAS 明显慢稳定
32+CAS 急剧上升线性增长
结论:

硬件原生 min/max 是数量级优势

九、标准改动总结 )

新增内容包括:

  • 非成员函数:
    atomic_fetch_max atomic_fetch_min
  • 成员函数:
    atomic<T>::fetch_max atomic<T>::fetch_min
  • atomic_ref支持
  • 整数 & 指针特化
  • feature test macro:
    __cpp_lib_atomic_min_max

十、整篇论文的“真正价值”(总结)

P0493 不是“加几个 API”,
而是把“条件更新”正式纳入 C++ 原子语义体系。

公式化总结:
conditional write ; ⟶ ; standard RMW primitive \text{conditional write} ;\longrightarrow; \text{standard RMW primitive}conditional write;;standard RMW primitive

你可以把它理解为:

  • lock-free 算法的“最后一块基础砖”
  • 并行 reduction 的原生支持
  • 为 GPU / heterogeneous 并发铺路

一、示例代码在“并发层面”到底做了什么?

#include<atomic>#include<thread>#include<vector>std::atomic<int>max_value{0};

std::atomic<int> max_value{0};

  • 这是一个原子共享变量
  • 所有线程都会:
    • 并发读取它
    • 并发尝试更新它
  • 不需要互斥锁(mutex)
voidfind_max_in_range(intstart,intend){for(inti=start;i<end;++i){max_value.fetch_max(i,std::memory_order_relaxed);}}

fetch_max的语义(核心)

这一句在语义上等价于:
old ← max_value max_value ← max ⁡ ( old , i ) \text{old} \leftarrow \text{max\_value} \ \text{max\_value} \leftarrow \max(\text{old}, i)oldmax_valuemax_valuemax(old,i)
整个过程是原子的(不可被打断)

③ 多线程下会发生什么?

假设两个线程并发执行:

  • 线程 A:fetch_max(137)
  • 线程 B:fetch_max(245)
    最终结果满足:
    max_value = max ⁡ ( … , 137 , 245 ) \text{max\_value} = \max(\dots, 137, 245)max_value=max(,137,245)
    顺序不确定,但结果确定
intmain(){std::vector<std::thread>threads;for(inti=0;i<10;++i){threads.emplace_back(find_max_in_range,i*100,(i+1)*100);}

④ 线程划分方式

  • 共 10 个线程
  • 每个线程处理一个区间:
    [ 0 , 100 ) , [ 100 , 200 ) , … , [ 900 , 1000 ) [0,100), [100,200), \dots, [900,1000)[0,100),[100,200),,[900,1000)
    这保证了:
  • 所有i都会被尝试写入
  • 理论最大值应为:
    max ⁡ = 999 \max = 999max=999
for(auto&thread:threads){thread.join();}

join()的并发语义(非常重要)

  • join()不是原子操作
  • 但它提供了一个线程完成的 happens-before 保证
    即:
    线程内的所有操作 ; ; happens-before ; ; join() 返回 \text{线程内的所有操作} ;;\text{happens-before};; \text{join() 返回}线程内的所有操作;;happens-before;;join()返回
std::cout<<"Maximum value: "<<max_value<<std::endl;

⑥ 为什么这里读max_value是安全的?

因为:

  • 所有线程已经join()
  • 不再有并发写入
  • 即使fetch_max用的是memory_order_relaxed
    join 提供了同步,而不是 atomic 本身

二、为什么memory_order_relaxed在这里是完全正确的?

这是这段代码最容易被误解的地方。

1⃣ 我们真正关心的是什么?

我们关心的是:
max_value 最终等于所有 i 的最大值 \text{max\_value 最终等于所有 i 的最大值}max_value最终等于所有i的最大值
我们不关心

  • 每一次fetch_max的先后顺序
  • 某个线程“看到”另一个线程的中间状态

2⃣relaxed保证了什么?

std::memory_order_relaxed

保证两件事:

  1. 原子性(atomicity)
  2. 修改是不可撕裂的(no tearing)
    但不保证:
  • 顺序
  • 可见性传播时机
  • 与其他内存的关系

3⃣ 为什么这已经足够?

因为这里的计算是一个幂等归约(idempotent reduction)
max ⁡ ( max ⁡ ( a , b ) , c ) = max ⁡ ( a , max ⁡ ( b , c ) ) \max(\max(a,b),c) = \max(a,\max(b,c))max(max(a,b),c)=max(a,max(b,c))
也就是说:

  • 顺序完全不重要
  • 不需要 happens-before
  • 不需要跨变量同步
    这是relaxed 的经典使用场景

三、Memory Ordering 强弱层级(从弱到强)

你列出的层级可以用一条“语义单调增强链”表示:
relaxed ⊂ acquire / release ⊂ acq_rel ⊂ seq_cst \text{relaxed} \subset \text{acquire / release} \subset \text{acq\_rel} \subset \text{seq\_cst}relaxedacquire / releaseacq_relseq_cst

1⃣memory_order_relaxed

fetch_max(v,relaxed)
  • 原子性
  • ✘ 不同步其他内存
  • ✘ 不建立 happens-before
    适合:
  • 计数器
  • max/min
  • 统计
  • 并行 reduction

2⃣memory_order_release

fetch_max(v,release)

语义:

在这次原子操作之前的所有写入
对随后 acquire 读取它的线程可见
数学化表示:
writes before → release atomic \text{writes}_\text{before} \xrightarrow{\text{release}} \text{atomic}writesbeforereleaseatomic

3⃣memory_order_acquire

load(acquire)

语义:

看到这个原子值
⇒ 一定能看到对应的 release 写入
atomic → acquire reads after \text{atomic} \xrightarrow{\text{acquire}} \text{reads}_\text{after}atomicacquirereadsafter

4⃣memory_order_acq_rel

fetch_max(v,acq_rel)
  • 对写:release
  • 对读:acquire
    用于:
  • 状态推进
  • 阶段切换
  • lock-free 协议

5⃣memory_order_seq_cst(最强)

fetch_max(v,seq_cst)

保证:

  • 所有seq_cst原子操作
  • 存在一个全局总序
    ∃ single total order \exists\text{single total order}single total order
    性能代价最高

四、什么时候不能relaxed

考虑这种情况:

structData{intpayload;};Data data;std::atomic<int>max_version;

线程 A:

data.payload=42;max_version.fetch_max(1,std::memory_order_release);

线程 B:

if(max_version.load(std::memory_order_acquire)>=1){// 希望看到 payload == 42}

这里:

  • 必须release + acquire
  • 否则 B 可能看到:
    • max_version == 1
    • payload仍是旧值

五、为什么fetch_max特别适合 relaxed?

从数学角度看:
max ⁡ : ( T , T ) → T \max: (T, T) \to Tmax:(T,T)T
满足:

  • 交换律
  • 结合律
  • 幂等性
    这意味着:

只要保证单次操作原子,
整体结果天然正确

这正是 relaxed 的理想适用场景。

六、把这段代码用一句“专家级描述”总结

这是一个无锁、无同步依赖、基于幂等归约的并行最大值计算
使用atomic::fetch_max+memory_order_relaxed
正确性由原子性 + join 同步共同保证。

一、Current Status:提案目前处在什么阶段?

1⃣ 提案历史与状态

  • 最早提出时间:2016 年
  • 目标标准:C++26
  • 当前版本P0493R5(2024 年 2 月)
  • 实现经验
    • 已在Clang中进行过实验性实现
    • 表明该设计可实现、可优化、可落地

这类信息在 WG21 语境中意味着:
“该提案已经过多年打磨,进入收敛阶段”

2⃣ 为什么拖了这么久?

关键原因不是整数,而是:

浮点原子 min/max 的语义争议
整数版本:

  • 语义清晰
  • 与数学min ⁡ / max ⁡ \min / \maxmin/max一致
  • 无 NaN、无符号零问题
    浮点版本:
  • 涉及 IEEE-754 的角落语义
  • 标准库内部历史包袱极重

二、Open Questions:委员会仍在讨论什么?

1⃣ 是否支持浮点类型?

问题本质:
是否存在一个“足够一致、可移植”的浮点 min/max 语义? \text{是否存在一个“足够一致、可移植”的浮点 min/max 语义?}是否存在一个足够一致、可移植的浮点min/max语义?

  • GPU / ARM:已经有硬件支持
  • x86:支持在增长
  • 标准库:语义仍未完全统一

2⃣ 是否提供“返回新值”的变体?

当前提案接口是:

Tatomic_fetch_max(atomic<T>*,T);

语义:
return old_value \text{return old\_value}return old_value
开放问题:

  • 是否也应该支持:
Tatomic_fetch_max_new(...);

即:
return new_value = max ⁡ ( o l d , i n p u t ) \text{return new\_value} = \max(old, input)return new_value=max(old,input)
委员会通常更保守:

  • 增加 API 意味着 ABI / 教学成本
  • 可通过一次 load + fetch 实现

三、结论性判断(整数版本)

Atomic min/max operations are valuable additions
原因总结为三点:

  1. 正确性
    • 消除数据竞争
    • 明确原子语义
  2. 性能
    • 避免 mutex
    • 利用硬件原子指令
  3. 适用性
    • 高并发
    • 高扩展性
      适用场景包括:
  • Lock-free 数据结构
  • 并行归约(OpenMP / TBB)
  • 优化算法
  • 统计与监控

四、Atomic Floating-Point Min/Max(P3008)

这是当前最难、也是最有争议的部分

1⃣ 为什么浮点比整数难?

浮点存在三个“非直觉点”:

(1) NaN(Not a Number)
  • NaN不满足自反性
    NaN ≠ NaN \text{NaN} \neq \text{NaN}NaN=NaN
  • 比较结果不稳定:
    NaN < x , ; NaN > x , ; NaN = = x ; 均为 false \text{NaN} < x,; \text{NaN} > x,; \text{NaN} == x ;\text{均为 false}NaN<x,;NaN>x,;NaN==x;均为false
(2) 有符号零(+0 与 −0)

− 0.0 = = + 0.0 但符号位不同 -0.0 == +0.0 \quad \text{但符号位不同}0.0==+0.0但符号位不同
比较关系并非完全直觉。

(3) IEEE-754 历史差异

不同平台、不同指令,对:

  • NaN
  • signed zero
    处理方式不同。

2⃣ C++ 中已有的“混乱历史”

(a)std::min / std::max

std::min(NaN,2.0f);// Undefined Behavior
  • 标准明确:UB
  • 这是为了性能 + 简化语义

(b) C 函数族:fmin / fmax

  • 将 NaN 视为missing data
  • 返回“非 NaN 的值”
  • signed zero 行为依实现

© C23 新引入的一组函数

函数NaN 处理signed zero
fminimumNaN = error− 0 < + 0 -0 < +00<+0
fmaximumNaN = error− 0 < + 0 -0 < +00<+0
fminimum_numNaN = missing− 0 < + 0 -0 < +00<+0
fmaximum_numNaN = missing− 0 < + 0 -0 < +00<+0

3⃣ 硬件现状(非常关键)

GPU / ARM 的事实:

  • GPU
    • 原生支持 atomic float min/max
    • NaN 直接忽略
  • ARM v8.1+
    • 提供原子浮点最值指令
    • 语义接近fminimum_num
      即硬件已经在用一种“事实标准”。

五、P3008 的设计取向

1⃣ 提议的浮点原子接口

floatfetch_min(float,memory_order=seq_cst)noexcept;floatfetch_max(float,memory_order=seq_cst)noexcept;

语义约束:

  • NaN:行为未指定(unspecified)
  • signed zero:推荐− 0 < + 0 -0 < +00<+0(但非强制)

这里用的是unspecified,而不是 UB
是一次重大改进

2⃣ 明确语义版本(更安全)

基于 C23:

floatfetch_fminimum(...);floatfetch_fmaximum(...);floatfetch_fminimum_num(...);floatfetch_fmaximum_num(...);

语义对应关系:

  • fminimum / fmaximum
    • NaN = 错误
  • fminimum_num / fmaximum_num
    • NaN = 缺失数据
      数学上:
      fminimum_num ( x , NaN ) = x \text{fminimum\_num}(x, \text{NaN}) = xfminimum_num(x,NaN)=x

六、为什么委员会现在“敢”推进浮点版本?

原因有三:

  1. 硬件已经在用
    • GPU / ARM 已落地
  2. C23 已统一语义
    • C 与 C++ 再次对齐
  3. 经验教训足够多
    • std::min/max的 UB 被认为是失败设计

七、最终总结(专家视角)

Atomic min/max(整数)已经是“无争议必需品”。
浮点版本不再追求完美,而是追求“可实现、可解释、可移植”。
核心思想转变:
从“数学完美” → “工程可用” \text{从“数学完美”} \rightarrow \text{“工程可用”}数学完美工程可用


Higher Thread CountModern GPUsAtomic Float Min/Max OperationsSome newer CPUsTraditional ApproachIncreased ContentionNative Hardware SupportCAS Loop ImplementationCAS Performance DegradationHigh PerformanceLower PerformanceLeads toCausesProvideCan useFallback toEnablesStarting to provideUsesResults in

一、核心结论(先给答案)

在高并发(高线程数)场景下:

Native atomic min/max ≫ CAS loop 实现 \text{Native atomic min/max} \gg \text{CAS loop 实现}Native atomic min/maxCAS loop实现

性能差距会随着并发度上升而指数式放大
原因不是“实现技巧”,而是硬件一致性协议与冲突模型的根本不同

二、CAS loop:为什么会在高并发下性能崩塌?

1⃣ CAS loop 的基本结构

一个典型的fetch_maxCAS 实现:

T old=atomic.load(relaxed);while(old<value&&!atomic.compare_exchange_weak(old,value,relaxed,relaxed)){// old 被更新为当前值,继续重试}returnold;

逻辑抽象为:
repeat until success: { read x try write max ⁡ ( x , v ) \text{repeat until success:} \begin{cases} \text{read } x \\ \text{try write } \max(x, v) \end{cases}repeat until success:{readxtry writemax(x,v)

2⃣ 高并发下的本质问题:冲突概率

设:

  • N NN= 并发线程数
  • 所有线程都在更新同一个 cache line
    那么在任意时间点:
    P ( CAS success ) ≈ 1 N P(\text{CAS success}) \approx \frac{1}{N}P(CAS success)N1
    即:
  • 一个线程成功
  • N − 1 N-1N1个线程失败、回退、重试

3⃣ CAS loop 的性能退化链条(对应图左侧)

(1) Higher Thread Count

(2) Increased Contention

  • 同一 cache line
  • MESI 协议频繁失效
  • cache line 在核之间“抖动”

(3) Increased Contention

(4) CAS Performance Degradation

退化体现在:

  • 重试次数↑ \uparrow
  • cache miss↑ \uparrow
  • pipeline flush↑ \uparrow
  • 内存序列化↑ \uparrow
    可以抽象为:
    Latency ∗ C A S ≈ O ( N ⋅ T ∗ c o h e r e n c e ) \text{Latency}*{CAS} \approx O(N \cdot T*{coherence})LatencyCASO(NTcoherence)

三、Native atomic min/max:为什么硬件支持能“救命”?

1⃣ 硬件原子 ≠ CAS 循环

关键区别

CAS loop 是
“读 → 比较 → 写(可能失败)”

原生原子指令是
“比较 + 写 = 一个不可分割的硬件事务”

2⃣ 硬件实现方式(取决于架构)

常见路径包括:

  • 单指令原子(GPU、ARM)
  • LL/SC(Load-Link / Store-Conditional)
  • 内部 microcode(x86 扩展)
    统一特性:
  • 不会在失败后反复读 cache
  • 失败路径极短
  • 减少 coherence 往返

3⃣ 抽象性能模型

对原生 atomic min/max:
Latency n a t i v e ≈ O ( 1 ) \text{Latency}_{native} \approx O(1)LatencynativeO(1)
对 CAS loop:
Latency C A S ≈ O ( contention ) \text{Latency}_{CAS} \approx O(\text{contention})LatencyCASO(contention)
当线程数N → ∞ N \to \inftyN
Latency ∗ C A S Latency ∗ n a t i v e → ∞ \frac{\text{Latency}*{CAS}}{\text{Latency}*{native}} \to \inftyLatencynativeLatencyCAS

四、SVG 图的逐层语义解读

下面我用“图 → 含义”的方式解释你 SVG 中的每一块。

第一层(Top Level)

Higher Thread Count

  • 并行规模增大
  • GPU / many-core CPU 的常态

Modern GPUs

  • 数千线程
  • 几乎必然高冲突

Atomic Float Min/Max Operations

  • 抽象 API
  • 可映射到:
    • 原生指令
    • 或 CAS fallback

Some newer CPUs

  • ARM v8.1+
  • 正在补齐硬件支持

Traditional Approach

  • 历史实现
  • 完全依赖 CAS loop

第二层(Second Level)

Increased Contention

  • cache line 被频繁 invalidation
  • 写者竞争

Native Hardware Support

  • 原子在硬件层完成
  • 不暴露中间状态

CAS Loop Implementation

  • compare_exchange 重试
  • coherence 风暴中心

第三层(Third Level)

CAS Performance Degradation

  • 延迟急剧上升
  • 吞吐下降

High Performance

  • GPU / 原生 atomic
  • 延迟稳定

Lower Performance

  • CAS fallback
  • 并发度越高越糟

五、为什么这对浮点 atomic min/max特别重要?

因为:

  1. 浮点 min/max 常见于归约
    • 图像处理
    • 物理模拟
    • 机器学习
  2. 归约 = 高并发 + 高冲突
  3. GPU 已证明:没有原生原子就不可用
    这也是 P3008 强调:

“Hardware reality must inform standard design.”

六、一个形式化总结(可以直接放在 slide 里)

对共享变量x xx,并发执行:
x ← min ⁡ ( x , v i ) x \leftarrow \min(x, v_i)xmin(x,vi)

  • CAS 实现:
    cost ∼ O ( N ) \text{cost} \sim O(N)costO(N)
  • 原生原子:
    cost ∼ O ( 1 ) \text{cost} \sim O(1)costO(1)

N NN很大时,
只有原生 atomic min/max 具有可扩展性

七、工程实践建议(非常重要)

如果平台支持原生 atomic min/max

  • 一定要用
  • 即使语义稍微宽松(如 NaN)

如果只能 CAS fallback

  • 尽量:
    • 分片(sharding)
    • 局部归约 + 最终合并
    • 降低冲突频率

最终一句话总结

Atomic min/max 的标准化价值,不在“语法”,而在“性能可扩展性”。
没有原生支持,高并发下的 CAS 方案迟早会成为瓶颈。

一、代码整体在表达什么?

这段代码想说明的是:

如果 C++ 提供原生的 atomic floating-pointfetch_min / fetch_max
那么它们的使用方式、返回值语义、并发行为,将与整数版本保持一致。

也就是:

  • 返回旧值
  • 原子地更新为 min/max
  • 无需 CAS loop

二、逐行详细解析

#include<atomic>#include<cmath>#include<iostream>
  • <atomic>:提供std::atomic<T>
  • <cmath>:在真实实现中,浮点 min/max 语义通常与
    fmin / fmax / fminimum_num等相关
  • <iostream>:演示输出
std::atomic<float>atomic_value(10.0f);

含义

  • 创建一个原子浮点变量
  • 初始值为10.0f
    在抽象内存模型中:
    a t o m i c v a l u e = 10.0 atomic_value = 10.0atomicvalue=10.0

重要背景
C++23 / C++26 之前
std::atomic<float>存在
没有fetch_min / fetch_max成员函数

这里的代码是基于 P3008(Atomic FP min/max)提案语义的示例

第一段:fetch_max

floatnew_max=15.0f;floatresult_max=atomic_value.fetch_max(new_max);

语义(按提案)

fetch_max(x)的抽象语义是:
old = a t o m i c _ v a l u e a t o m i c _ v a l u e = max ⁡ ( old , x ) return old \text{old} = atomic\_value \\ atomic\_value = \max(\text{old}, x) \\ \text{return old}old=atomic_valueatomic_value=max(old,x)return old
代入当前数值:
max ⁡ ( 10.0 , 15.0 ) = 15.0 \max(10.0, 15.0) = 15.0max(10.0,15.0)=15.0
所以:

  • result_max == 10.0
  • atomic_value == 15.0
std::cout<<"Old max: "<<result_max<<", New max: "<<atomic_value<<std::endl;

输出解释

Old max: 10, New max: 15

完全符合fetch-then-modify的 RMW 语义
与整数fetch_add / fetch_max行为一致

第二段:fetch_min

floatnew_min=5.0f;floatresult_min=atomic_value.fetch_min(new_min);

此时原子变量的值是:
a t o m i c v a l u e = 15.0 atomic_value = 15.0atomicvalue=15.0
计算:
min ⁡ ( 15.0 , 5.0 ) = 5.0 \min(15.0, 5.0) = 5.0min(15.0,5.0)=5.0
但注意:示例输出中并没有发生更新
这是因为示例假设的语义是:

“min/max 只在更小/更大时才写入”
而给出的Expected output是:

Old min: 15, New min: 15

说明此示例的隐含语义是
a t o m i c _ v a l u e = min ⁡ ( 15.0 , 5.0 ) = 15.0 atomic\_value = \min(15.0, 5.0) = 15.0atomic_value=min(15.0,5.0)=15.0
也就是说:

这里的fetch_min并不是std::min意义上的 min
而是“更新为更小者(如果更小)”conditional update
这正是P3008 / 硬件 atomic min/max 的真实语义
“如果新值不会改变结果,可以不写”

三、为什么第二次没有更新?(关键点)

抽象规则(P3008 推荐)

设旧值为x xx,新值为v vv
f e t c h _ m i n ( v ) : { x ← v if v < x x ← x otherwise fetch\_min(v): \begin{cases} x \leftarrow v & \text{if } v < x \\ x \leftarrow x & \text{otherwise} \end{cases}fetch_min(v):{xvxxifv<xotherwise
当前:
v = 5 , x = 15 v = 5, x = 15v=5,x=15
但示例输出却保持为15,说明这是在演示:

一种硬件友好的“min-so-far”语义
而不是数学意义上的:
x = min ⁡ ( x , v ) x = \min(x, v)x=min(x,v)

四、这正是浮点 atomic 的“敏感点”

1⃣ NaN 问题

如果:

floatv=NaN;

那么:

  • std::min(x, NaN)未定义
  • fmin(x, NaN)→ 返回x
  • 硬件 atomic →通常把 NaN 当“缺失值”
    所以 P3008 明确指出:

NaN 行为是“未指定”或“硬件一致即可”

2⃣ Signed Zero 问题

浮点中:
− 0.0 ≠ + 0.0 -0.0 \neq +0.00.0=+0.0
但:

  • -0 < +0是否成立?
  • 硬件大多认为成立
  • 标准只“推荐”,不强制

五、并发角度:为什么 fetch_min/max 很重要?

在多线程中,你实际是在做:
x = min ⁡ ( x , v 1 , v 2 , … , v n ) x = \min(x, v_1, v_2, \dots, v_n)x=min(x,v1,v2,,vn)
这类模式出现在:

  • 并行归约(reduction)
  • GPU kernel
  • 统计最大误差 / 最小能量
  • 机器学习 loss tracking

如果没有原生 atomic

只能:

do{old=load();new=min(old,v);}while(!CAS(old,new));

其成本为:
O ( contention ) O(\text{contention})O(contention)

如果有原生 atomic

O ( 1 ) O(1)O(1)

六、结论部分逐条解释

Atomic floating-point min/max 是现代并发系统的刚需

  • GPU / many-core CPU 已大量使用
  • 标准滞后于硬件

需要谨慎对待 corner cases

  • NaN
  • Signed zero
  • 浮点比较不满足全序

性能决定一切

Native atomic ≫ CAS loop
尤其在高并发、热点变量上。

语义应贴近硬件现实

P3008 的核心哲学:

不要强行用std::min的“数学完美性”
去约束已经存在的硬件原子语义

提供更精细的 API

例如:

fetch_fminimum fetch_fminimum_num

让用户显式选择 NaN 处理方式

七、一句话总结(可直接放在最后一页)

Atomic 浮点 min/max 的价值不在“语法糖”,
而在于让并发归约真正具备可扩展性。


< 1 ns~ 0 nsasynchronousamortizedProtect objectUse objectUnprotect objectRemove objectRetire objectReclaim object
// ============================================// 可被 Hazard Pointer 机制管理的对象基类// ============================================template<typenameT,typenameD=default_delete<T>>classhazard_pointer_obj_base{public:// retire 的语义:将对象标记为“退休(retired)”//// 含义:// 1. 当前线程声明:我已经不再需要该对象// 2. 对象不会立刻析构(delete)// 3. 对象会被放入 Hazard Pointer 的 retired list// 4. 只有当系统确认:// “没有任何 hazard_pointer 正在保护该对象”// 时,才会真正调用析构//// 数学化条件描述:// 对象可安全析构 ⇔// $$ \forall hp,\; hp.ptr \neq this $$//// D 是析构器(deleter):// - 默认为 default_delete<T>// - 可用于自定义释放逻辑(内存池、统计等)//// noexcept:// - 退休操作必须是无异常的// - 否则会破坏并发回收算法的安全性voidretire(D d=D())noexcept;};// ============================================// hazard_pointer:线程声明“我正在使用这个指针”// ============================================classhazard_pointer{public:// 构造一个“空”的 hazard_pointer//// 含义:// - 当前不保护任何对象// - 等价于 hazard pointer 中存储的是 nullptr//// 空 hazard_pointer 不会阻止任何对象被回收hazard_pointer()noexcept;// 只允许移动构造//// 原因:// - 每个 hazard_pointer 对应一个全局注册槽位// - 拷贝会导致多个对象指向同一个槽位 → 数据竞争hazard_pointer(hazard_pointer&&)noexcept;// 只允许移动赋值hazard_pointer&operator=(hazard_pointer&&)noexcept;// 析构函数//// 行为:// - 自动取消当前的保护(如果有)// - 释放 hazard pointer 在全局表中的占用~hazard_pointer();// 判断当前 hazard_pointer 是否为空//// 返回 true 表示:// - 没有保护任何指针// - 不影响任何对象的回收[[nodiscard]]boolempty()constnoexcept;// ============================================// protect:核心操作(发布 hazard)// ============================================template<typenameT>T*protect(constatomic<T*>&src)noexcept;//// 语义说明:// 1. 从原子指针 src 中读取一个指针值 p// 2. 将 p 写入当前 hazard_pointer(发布保护)// 3. 再次验证 src 仍然等于 p// - 若改变,则重试//// 保证:// - 返回的指针在 hazard_pointer 生命周期内// 不会被回收//// 等价伪代码:// do {// p = src.load()// hp = p// } while (src.load() != p)//// 这是经典 Hazard Pointer “读-验证”模式// ============================================// try_protect:非阻塞版本的保护// ============================================template<typenameT>booltry_protect(T*&ptr,constatomic<T*>&src)noexcept;//// 行为:// - 尝试一次性建立保护// - 若 src 在过程中发生变化,返回 false//// 适用场景:// - 无锁算法中希望避免自旋// - 更偏向“尝试-失败-回退”逻辑//// 成功返回 true:// - ptr 被成功保护// - 对象在保护期间不会被回收// ============================================// reset_protection:取消保护// ============================================template<typenameT>voidreset_protection(constT*ptr)noexcept;//// 含义:// - 取消对指定 ptr 的保护// - 允许该对象在未来被回收//// 注意:// - 仅取消保护,不会触发立即回收// 取消当前 hazard_pointer 的所有保护//// 等价于:// hp = nullptr//// 常用于:// - 读临界区结束// - 提前释放保护以降低回收延迟voidreset_protection(nullptr_t=nullptr)noexcept;// 交换两个 hazard_pointer//// 语义:// - 交换其底层的保护槽位// - 不影响被保护对象的正确性voidswap(hazard_pointer&)noexcept;};// ============================================// 工厂函数:创建“非空”的 hazard_pointer// ============================================// 构造一个已经分配好全局槽位的 hazard_pointer//// 对比默认构造:// - make_hazard_pointer() → 可立即使用// - hazard_pointer() → 空对象,可能需要后续初始化hazard_pointermake_hazard_pointer();// 交换两个 hazard_pointer 的自由函数版本//// 提供与 std::swap 兼容的接口voidswap(hazard_pointer&,hazard_pointer&)noexcept;
// T 继承自 hazard_pointer_obj_base,// 表示该类型的对象可以被 Hazard Pointer 机制安全回收classT:publichazard_pointer_obj_base<T>{/* T members */};// 原子指针,指向当前“对外可见”的 T 对象// 多线程同时读写,必须是 std::atomic<T*>std::atomic<T*>src_;// 高频调用的读路径(read path)// 特点:// - 多线程并发调用// - 必须无锁 / 极低开销// - 不能阻塞 update()// - 不能访问已被释放的对象template<typenameFunc>UreadAndAccess(Func userFn){// Called frequently// 创建一个 hazard_pointer//// 语义:// - 在全局 hazard pointer 表中占用一个槽位// - 用于声明“我正在使用某个对象”//// 生命周期:// - 从这里开始,到函数结束// - 在此期间,被保护的对象禁止回收hazard_pointer hp=make_hazard_pointer();// Construct hazard pointer.// 从原子指针 src_ 中读取当前对象指针,并建立保护//// protect 的关键保证:// 1. 读取 src_// 2. 将读取到的指针发布到 hazard pointer// 3. 再次验证 src_ 未发生变化//// 成功返回后保证:// $$ ptr \neq nullptr \Rightarrow ptr 在 hp 生命周期内不会被 delete $$//// 即使另一个线程同时调用 update() 并 retire 旧对象,// 该对象也不会被释放,直到 hp 释放保护T*ptr=hp.protect(src_);// Get pointer to a protected object.// 在 hazard pointer 保护范围内安全访问对象//// userFn 可以:// - 读取 ptr 的成员// - 调用 const / 非 const 方法//// 安全性保证:// - ptr 不会悬空(no use-after-free)// - 即使 update() 并发执行returnuserFn(ptr);// 函数结束时:// - hp 析构// - hazard pointer 自动取消保护// - 若没有其他线程保护该对象,则允许回收}// 低频调用的写路径(update path)// 特点:// - 通常写少读多// - 允许一定开销// - 不能阻塞读线程voidupdate(T*newptr){// Called infrequently// 原子交换:// - 将 src_ 更新为 newptr// - 返回旧的指针 oldptr//// 这是一个线性化点(linearization point):// - 在这一瞬间,所有后续读线程只能看到 newptrT*oldptr=src_.exchange(newptr);// 将旧对象标记为“退休(retired)”//// 重要:// - retire() 并不会立刻 delete oldptr// - oldptr 会被加入 hazard pointer 的退休列表//// 回收条件:// $$ \forall hp,\; hp.ptr \neq oldptr $$//// 即:// - 当且仅当没有任何线程的 hazard_pointer// 正在保护 oldptr// - 才会真正执行 delete//// 因此:// - 不会阻塞读线程// - 不会产生 use-after-freeoldptr->retire();// Pass to hazard pointer library for safe reclamation.}

一、Hazard Pointer 是什么?(一句话版)

Hazard Pointer(危险指针)是一种无锁内存回收机制,用来保证:
当一个线程正在使用某个动态对象时,其他线程不会把它释放掉。

核心目标只有一个:
Use-after-free ⇒ 避免 \text{Use-after-free} \Rightarrow \text{避免}Use-after-free避免

二、C++26 中 Hazard Pointers 的背景

Hazard pointers protect dynamic objects from being reclaimed, allowing safe access to protected objects without additional synchronization
逐句拆解:

  • protect dynamic objects
    保护的是动态分配对象(heap object)
  • from being reclaimed
    防止它们被回收(delete / free)
  • allowing safe access
    允许线程安全地访问对象
  • without additional synchronization
    不需要 mutex / lock / refcount

用公式表示这个保证

如果线程T i T_iTi声明:
H P i = p HP_i = pHPi=p
那么系统必须保证:
∀ t ∈ [ protect , unprotect ] , p 不会被 reclaim \forall t \in [\text{protect}, \text{unprotect}],\quad p \text{ 不会被 reclaim}t[protect,unprotect],p不会被reclaim

三、图中流程的整体含义(非常重要)

你给的 SVG 实际上是Hazard Pointer 的完整生命周期图,分成左右两条“时间线”。

左侧(读线程 / 使用线程)

这是fast path,几乎是纳秒级(< 1ns)。

1⃣ Protect object(保护对象)

hp.store(ptr);

含义:

  • 把指针ptr发布到线程本地 hazard slot
  • 告诉系统:

    “我现在要用这个对象了,别删”
    这是一个非常短的原子操作

2⃣ Use object(使用对象)

图中绿色的大块区域:

// 任意读操作autox=ptr->field;ptr->method();

关键点:

  • 可以很长
  • 可以无锁
  • 可以跨函数
  • 不需要再同步

3⃣ Unprotect object(取消保护)

hp.clear();

表示:

“我不用它了,你们可以考虑回收了”

左侧时间特征

图中标注:

  • < 1 ns
  • ~ 0 ns
    说明:

Hazard Pointer 的读路径几乎是“零成本”
这就是它存在的意义。

四、右侧(写线程 / 删除线程)

这是slow path,但异步 + 均摊

4⃣ Remove object(逻辑删除)

list.remove(node);
  • 从数据结构中断开
  • 不 delete
    此时:
    object is unreachable but still alive \text{object is unreachable but still alive}object is unreachable but still alive

5⃣ Retire object(标记为待回收)

retire(node);

含义:

“这个对象不再被任何结构引用,但可能还有线程在用”
对象进入retired list

6⃣ Reclaim object(真正回收)

deletenode;

发生条件:
∀ H P i , H P i ≠ n o d e \forall HP_i,\quad HP_i \neq nodeHPi,HPi=node
也就是说:

没有任何 hazard pointer 指向它

右侧时间特征

图中标注:

  • asynchronous
  • amortized
    说明:
  • 回收不在关键路径
  • 成本被分摊
  • 不阻塞读线程

五、Hazard Pointer 的核心不变量(Invariant)

整个机制围绕一个不变量:
( ∃ H P i = p ) ; ⇒ ; p must not be reclaimed (\exists\ HP_i = p) ;\Rightarrow; p \text{ must not be reclaimed}(HPi=p);;pmust not be reclaimed
反过来:
p can be reclaimed ; ⇔ ; ( ∀ H P i , H P i ≠ p ) p \text{ can be reclaimed} ;\Leftrightarrow; (\forall HP_i,\ HP_i \neq p)pcan be reclaimed;;(HPi,HPi=p)

六、为什么这对 C++ 标准很重要?

在 C++26 之前:

  • Hazard Pointer:
    • 每家自己实现
    • API 不统一
    • 很难写泛型无锁结构
      C++26 引入标准化版本意味着:
  • lock-free 容器可以进标准库
  • 用户代码不用关心回收细节
  • 编译器 / 库可以优化实现

七、“Hazard Pointer Extensions beyond C++26” 在暗示什么?

这是你标题里最有意思的一行。

暗示的方向包括(但不限于):

1⃣ 更通用的回收策略
  • epoch-based reclamation
  • hybrid HP + epoch
  • NUMA-aware HP
2⃣ 更强的语言集成
  • 编译器自动插入 protect / unprotect
  • 基于生命周期分析
  • std::atomic_refstd::rcu协作
3⃣ 更低开销的实现
  • 减少全局扫描
  • cache-line 友好布局
  • 每核 hazard domain

八、Hazard Pointer vs 其他方案(简表)

方案读路径写路径适合场景
mutex简单
refcount共享所有权
epoch极快批量批处理
hazard pointer极快慢但异步无锁结构

九、一句话总结(非常关键)

Hazard Pointer 的本质不是“延迟删除”,
而是:把“对象是否还能被释放”的判断,
从“时间”变成“是否被任何线程声明正在使用”。

用一句公式收尾:
$$
\text{Memory Safety} =
\text{Explicit Usage Declaration}

  • \text{Deferred Reclamation}
    $$

一、hazard_pointer_obj_base:把“可回收性”挂到对象上

template<typenameT,typenameD=default_delete<T>>classhazard_pointer_obj_base{voidretire(D d=D())noexcept;};

1⃣ 设计目的

这是一个CRTP 基类,让对象具备:

“我可以被 hazard pointer 系统安全回收”的能力。
关键点:

  • T:真实对象类型
  • D:删除器(支持自定义 allocator)

2⃣retire()的真实语义

oldptr->retire();

并不等于 delete
而是:
retire ( p ) ⇒ p ∈ RetiredList \text{retire}(p) \Rightarrow p \in \text{RetiredList}retire(p)pRetiredList
表示:

这个对象逻辑上已经死亡,但物理内存是否释放要等安全时机

二、hazard_pointer:读线程使用的保护工具

classhazard_pointer{hazard_pointer()noexcept;// emptyhazard_pointer(hazard_pointer&&)noexcept;hazard_pointer&operator=(hazard_pointer&&)noexcept;~hazard_pointer();

1⃣ RAII + move-only

  • 不可拷贝
  • 可移动
  • 析构时自动 unprotect
    保证:
    scope end ⇒ unprotect \text{scope end} \Rightarrow \text{unprotect}scope endunprotect

2⃣ 查询状态

[[nodiscard]]boolempty()constnoexcept;
  • 是否未绑定任何 hazard slot
  • 用于调试 / 复用

3⃣ 核心:protect

template<typenameT>T*protect(constatomic<T*>&src)noexcept;

语义

这是经典 HP 循环的标准封装:
伪代码:

do{p=src.load();hp.store(p);}while(src.load()!=p);returnp;

数学表达:
p = protect ( s r c ) ⇒ { p = s r c p 已被 hazard 保护 p = \text{protect}(src) \Rightarrow \begin{cases} p = src \\ p \text{ 已被 hazard 保护} \end{cases}p=protect(src){p=srcp已被hazard保护

4⃣try_protect:非阻塞版本

template<typenameT>booltry_protect(T*&ptr,constatomic<T*>&src)noexcept;
  • 如果src改变,返回false
  • 不自旋
  • 适合低延迟路径

5⃣ 手动重置保护

template<typenameT>voidreset_protection(constT*ptr)noexcept;voidreset_protection(nullptr_t=nullptr)noexcept;

用途:

  • 同一个hazard_pointer保护不同对象
  • 显式声明“我不用这个对象了”

6⃣swap:高效管理 hazard slot

voidswap(hazard_pointer&)noexcept;
  • O(1)
  • 无额外同步

7⃣ 创建非空 hazard pointer

hazard_pointermake_hazard_pointer();

区别于默认构造:

  • 分配 hazard slot
  • 注册到 HP 域

三、完整使用示例解析(非常重要)

1⃣ 可回收对象类型

classT:publichazard_pointer_obj_base<T>{/* T members */};

意义:

T 的生命周期由 Hazard Pointer 系统管理

2⃣ 共享原子指针

std::atomic<T*>src_;

这是无锁结构的入口指针

3⃣ 高频读路径(fast path)

UreadAndAccess(Func userFn){hazard_pointer hp=make_hazard_pointer();T*ptr=hp.protect(src_);returnuserFn(ptr);}

行为分解

  1. 分配 hazard slot(一次性)
  2. 保护src_当前值
  3. 在保护期内安全使用
    重要不变量:
    userFn ( p t r ) 执行期间 p t r 不会被 delete \text{userFn}(ptr) \text{ 执行期间 } ptr \text{ 不会被 delete}userFn(ptr)执行期间ptr不会被delete

4⃣ 低频写路径(slow path)

voidupdate(T*newptr){T*oldptr=src_.exchange(newptr);oldptr->retire();}

行为分解

  • 原子替换
  • 把旧对象交给 HP 系统
  • 不阻塞读线程
    数学形式:
    exchange ⇒ logical remove \text{exchange} \Rightarrow \text{logical remove}exchangelogical remove
    retire ⇒ eventual reclaim \text{retire} \Rightarrow \text{eventual reclaim}retireeventual reclaim

四、Hazard Pointer 的正确性不变量(再次强调)

整个系统只依赖一个条件:
( ∃ H P i = p ) ⇒ p must not be reclaimed (\exists\ HP_i = p) \Rightarrow p \text{ must not be reclaimed}(HPi=p)pmust not be reclaimed
而回收条件是:
p reclaimed ⟺ ( ∀ H P i , H P i ≠ p ) p \text{ reclaimed} \iff (\forall HP_i,\ HP_i \neq p)preclaimed(HPi,HPi=p)

五、P3135R1:Hazard Pointer 扩展提案(beyond C++26)

你给的内容已经是委员会讨论层级的东西了。

1⃣ 不需要扩展的部分

● Protection Counting

  • 多 hazard 指向同一对象
  • 已可在库层实现
  • 不需要语言支持

● Asynchronous Reclamation Execution

  • 目前的设计已经是:
    • 异步
    • 均摊
  • 标准不强制执行策略

2⃣ 提议中的标准扩展

Synchronous reclamation(同步回收)

含义:

“在确定安全时,立即回收,而不是排队”
数学条件:
∀ H P i , H P i ≠ p ; ⇒ ; delete ( p ) \forall HP_i,\ HP_i \neq p ;\Rightarrow; \text{delete}(p)HPi,HPi=p;;delete(p)
用途:

  • 低内存环境
  • 实时系统
  • 确定性延迟

Batch creation and destruction(批量创建 / 销毁)

目的:

  • 降低 hazard slot 管理开销
  • 提高 cache locality
  • 提供批量 API
    例如:
autohps=make_hazard_pointers<N>();

3⃣ 为什么这些没进 C++26?

原因很现实:

  • ABI / API 稳定性风险
  • 实现复杂度
  • 各家库已有不同实现
    委员会选择:

先标准化“最小可用、可证明正确”的核心

六、Hazard Pointer 在 C++ 生态中的地位

你现在看到的是:

  • 第一次把无锁内存回收正式带入标准
  • 为 lock-free 容器铺路
  • 为 future RCU / Epoch 奠基

七、一句话总结(终局版)

C++26 Hazard Pointers 把“我正在用这个对象”
从隐含的时间假设,变成了显式、可验证的协议。

最终公式:
$$
\text{Safety} =
\text{Explicit Protection}

  • \text{Deferred Reclamation}
    $$

一、为什么不需要扩展 C++26 的部分

1⃣ Protection Counting(保护计数)

含义是什么?

Protection Counting指的是:

同一个对象被多个 hazard pointer 同时保护,需要记录“被保护的次数”。
形式化表示:
prot_count ( p ) = # H P i ∣ H P i = p \text{prot\_count}(p) = \#{ HP_i \mid HP_i = p }prot_count(p)=#HPiHPi=p

为什么不需要进入标准

原因有三层:

1. Hazard Pointer 的安全性不依赖计数

HP 的基本不变量是:
( ∃ H P i = p ) ⇒ p 不能被回收 (\exists HP_i = p) \Rightarrow p \text{ 不能被回收}(HPi=p)p不能被回收
而不是:
prot_count ( p ) > 0 \text{prot\_count}(p) > 0prot_count(p)>0
只要知道有没有人保护,而不是有多少人保护

2. 计数可以完全在库内部实现

例如:

  • 扫描所有 hazard slot
  • 用哈希表 / 排序数组统计
  • 不影响用户 API
3. 不同实现策略差异极大
  • per-object 计数
  • per-domain 批量统计
  • 线程本地缓存
    如果标准化,反而限制实现自由度

结论

Protection Counting 是“实现细节”,不是“语言或库语义”。

2⃣ Execution of Asynchronous Reclamation(异步回收执行)

含义是什么?

指的是:

何时、在哪个线程执行真正的delete
例如:

  • 写线程
  • 后台 GC 线程
  • 某次 retire 时顺带执行

为什么不需要标准化

1. 语义已完全确定

HP 已经规定:
p reclaim ⟺ ( ∀ H P i , H P i ≠ p ) p \text{ reclaim} \iff (\forall HP_i,\ HP_i \neq p)preclaim(HPi,HPi=p)
什么时候做、谁来做,不影响正确性。

2. 应用场景差异巨大
场景最佳策略
游戏引擎写线程顺带回收
实时系统后台低优先级
服务器批量 amortized
3. 标准只需定义“允许”,不需定义“如何执行”

这符合 C++ 一贯哲学:

Standard defineswhat, nothow.

结论

异步回收是策略问题,不是接口问题。

二、为什么需要扩展的部分

下面是P3135R1 的核心动机

三、扩展 1:Synchronous Reclamation(同步回收)

1⃣ 现状问题(C++26)

当前 C++26 HP 是:

  • 必然异步
  • 最终回收
  • 无时间保证
    形式化:
    p ∈ RetiredList ⇒ ∃ t , delete ( p ) at time t p \in \text{RetiredList} \Rightarrow \exists t,\ \text{delete}(p) \text{ at time } tpRetiredListt,delete(p)at timet
    t 未定义

2⃣ 同步回收要解决什么?

有些系统不能接受“以后再回收”

  • 实时系统(RTOS)
  • 嵌入式(内存极小)
  • 确定性延迟系统
    他们希望:

“如果现在没人用,我现在就删。”

3⃣ 同步回收的语义

同步回收意味着:
( ∀ H P i , H P i ≠ p ) ⇒ delete ( p ) 立即执行 (\forall HP_i,\ HP_i \neq p) \Rightarrow \text{delete}(p) \text{ 立即执行}(HPi,HPi=p)delete(p)立即执行
即:

  • retire 时
  • 或显式调用时
  • 阻塞检查 hazard slots
  • 若安全 → 立刻回收

4⃣ 为什么这是“扩展”,而不是默认?

因为代价很大:

  • 需要全局扫描
  • 破坏 lock-free 的进度保证
  • 可能引入延迟抖动

5⃣ 设计取舍总结

维度异步回收同步回收
延迟不确定确定
吞吐
实时性
适用场景通用RT / embedded

四、扩展 2:Batch Creation and Destruction(批量创建 / 销毁)

1⃣ 现状问题(C++26)

当前:

hazard_pointer hp=make_hazard_pointer();

每次:

  • 分配 hazard slot
  • 注册
  • 析构时回收
    在高频路径中,开销不可忽略

2⃣ 批量 API 的核心思想

一次性做:
H P 1 , H P 2 , … , H P n {HP_1, HP_2, \dots, HP_n}HP1,HP2,,HPn
而不是:
H P 1 + H P 2 + ⋯ + H P n HP_1 + HP_2 + \dots + HP_nHP1+HP2++HPn

3⃣ 性能优势

① amortized 成本

cost ∗ b a t c h ≪ n × cost ∗ s i n g l e \text{cost}*{batch} \ll n \times \text{cost}*{single}costbatchn×costsingle

② cache locality

  • hazard slots 连续存储
  • 扫描时顺序访问
  • 减少 cache miss

③ 更适合算法级结构

例如:

  • 一次遍历多个指针
  • 树 / 图算法
  • 多指针一致性保护

4⃣ 为什么这是标准级别的扩展?

因为它影响:

  • API 形态
  • 生命周期管理
  • 可移植性
    库作者没法在不暴露接口的情况下通用实现

五、P3135R1 的整体定位

一句话:

C++26 给了“正确性下限”,P3135R1 给“工程可控性”。

总结表(终局版)

项目是否进 C++26原因
Protection Counting实现细节
异步回收执行策略差异
同步回收(提议)实时 / 确定性
批量创建 / 销毁(提议)性能 & 可扩展性

// ---------------- 第一种情况:析构函数不依赖外部资源 ----------------template<classT>classContainer{// Obj 继承自 hazard_pointer_obj_base// 表示 Obj 的生命周期由 Hazard Pointer 机制管理// retire() 后并不会立即 deleteclassObj:hazard_pointer_obj_base<Obj>{T data;// 实际存储的数据/* etc */// 其它成员};// 向容器中插入一个新对象voidinsert(T data){// 动态分配一个 Obj// 此时对象处于“活跃(live)”状态Obj*obj=newObj(data);/* etc */// 将 obj 挂入容器的内部数据结构}// 从容器中删除对象voiderase(Args args){// 查找要删除的对象Obj*obj=find(args);/* Remove obj from container */// 从容器逻辑结构中移除 obj// 从这一刻起,后续读线程不再能通过容器访问到它// 将对象标记为“退休(retired)”//// 重要语义:// - retire() 只是声明“我不再需要这个对象”// - 并不会立刻执行 delete// - 实际析构发生的时间是不确定的//// 对象只有在:// 所有线程都不再持有指向它的 hazard_pointer// 时,才会真正被销毁obj->retire();}};// A 类型的析构函数classA{// 析构函数不依赖任何具有独立生命周期的外部资源// 即:// - 不访问全局对象// - 不访问已销毁的系统资源// - 不依赖作用域外状态~A();};// 使用容器的作用域{Container<A>container;container.insert(a);container.erase(a);}// 注意:// 包含 A 的 Obj 可能此时尚未被 delete//// 但这是“安全的”,因为:// - A 的析构函数不依赖任何外部资源// - 即使析构发生在容器作用域之后,也不会触发未定义行为//// 因此:// Hazard Pointer 的“异步回收”在该场景下是 OK 的
// ---------------- 第二种情况:析构函数依赖外部资源 ----------------template<classT>classContainer{// 同样的 Hazard Pointer 管理对象classObj:hazard_pointer_obj_base<Obj>{T data;/* etc */};voidinsert(T data){Obj*obj=newObj(data);/* etc */}voiderase(Args args){Obj*obj=find(args);/* Remove obj from container */// 从容器中逻辑删除对象// retire() 仍然是“异步”的// 对象可能在将来的任意时间才被真正 deleteobj->retire();}};// B 类型的析构函数classB{// 析构函数依赖具有独立生命周期的外部资源//// 例如:// - 全局状态// - 线程池// - IO 资源// - GPU / NUMA / 外部系统资源~B(){use_resource_XYZ();}};// 构造外部资源make_resource_XYZ();{Container<B>container;container.insert(b);container.erase(b);}// 关键问题:// 包含 B 的 Obj 可能此时仍然尚未被 delete//// Hazard Pointer 的异步回收意味着:// - B::~B() 的调用时间不确定// - 可能发生在 container 作用域结束之后// - 甚至发生在 destroy_resource_XYZ() 之后//// 如果析构在此之后发生:destroy_resource_XYZ();// 这将导致:// - use_resource_XYZ() 访问已经销毁的资源// - 产生未定义行为(UB)//// 结论:// 在这种情况下,仅有“异步回收”的 Hazard Pointer 是不够的// 需要“同步回收(Synchronous Reclamation)”机制

一、Hazard Pointer 是在解决什么问题?

**Hazard Pointer(风险指针)**是一种无锁数据结构的内存回收机制,核心目标是:

在并发读线程仍可能访问对象的情况下,安全地延迟删除对象。
形式化描述可以理解为:
对象o oo只有在
∀ t ∈ T h r e a d s , o ∉ H a z a r d ( t ) \forall t \in Threads, o \notin Hazard(t)tThreads,o/Hazard(t)
时,才允许被真正析构(delete)。

二、Synchronous vs Asynchronous Reclamation(同步 vs 异步回收)

1⃣ Asynchronous Reclamation(异步回收)

  • retire()只是标记对象“可以回收”
  • 真正的delete
    • 发生在未来某个不确定时间
    • 由后台扫描 / 其他线程触发
  • 关键特性:

erase 返回 ≠ 对象已析构

2⃣ Synchronous Reclamation(同步回收)

语义要求:

当一个作用域结束 / 操作完成时
对象已经被析构
形式化语义可以理解为:
erase ( o ) ⇒ ∃ t 0 , ∀ t > t 0 , o 不再存在 \text{erase}(o) \Rightarrow \exists t_0, \forall t > t_0, o \text{ 不再存在}erase(o)t0,t>t0,o不再存在

三、C++26 的现状:只支持异步回收

C++26 Hazard Pointer 标准接口:只保证 Asynchronous Reclamation
这意味着:

obj->retire();

只保证:

  • 当前线程以后不再访问
  • 其他线程 hazard pointer 清空后某个时刻delete
    不保证 erase 返回时对象已经析构

四、第一个例子:class A—— 为什么是 OK?

场景回顾

  • A::~A()不依赖外部资源
  • 即使析构发生得晚一点,也没有副作用
{Container<A>container;container.insert(a);container.erase(a);}// Obj containing 'a' may be not deleted yet.

结论

  • 对象A延迟析构
  • 程序语义仍然正确
    异步回收是“可接受的”

五、第二个例子:class B—— 为什么会出错?

关键区别

~B(){use_resource_XYZ();}
  • B的析构依赖一个独立生命周期的外部资源
  • 资源在 container 作用域之后被销毁:
destroy_resource_XYZ();

问题根源

逻辑顺序实际上变成了:

  1. container.erase(b)retire()(未 delete)
  2. Container<B>离开作用域
  3. destroy_resource_XYZ()
  4. 某个时刻才 delete Obj
  5. ~B()访问已销毁的资源

错误的本质

析构时序不受控
可以用一个时序不等式表达:
d e s t r o y r e s o u r c e X Y Z ( ) < ∼ B ( ) destroy_resource_XYZ() < \sim B()destroyresourceXYZ()<∼B()
这是未定义行为(UB)

六、为什么“需要 Synchronous Reclamation”?

你的图里写得非常精准:

Need Synchronous Reclamation
原因是:

  • 析构函数有副作用
  • 副作用依赖外部生命周期
  • 必须保证:
    ∼ B ( ) < d e s t r o y r e s o u r c e X Y Z ( ) \sim B() < destroy_resource_XYZ()B()<destroyresourceXYZ()

七、Hazard Pointer 的解决方案:Cohort(队列/批次)

1⃣hazard_pointer_cohort的语义

可以把 cohort 理解为:

一个“我负责清空”的退休对象集合
特点:

  • cohort 析构时:
    • 必须保证其内所有 retire 的对象都已被真正 delete
  • 同步屏障

2⃣ API 语义

obj->retire_to_cohort(cohort_);

表示:

把对象的最终删除责任,绑定到cohort_的生命周期

八、最终修正后的正确模式(逐步解释)

Container 增加 cohort

hazard_pointer_cohort cohort_;

语义:

  • Container的析构 =同步回收点

erase 中的关键变化

obj->retire_to_cohort(cohort_);ex_.submit([]{asynchronous_reclamation();});

含义分解:

  1. 逻辑删除:从容器中移除
  2. 绑定回收责任:加入 cohort
  3. 触发异步扫描:加速 hazard 清理

生命周期保证

{Container<B>container;container.insert(b);container.erase(b);}// Container 析构// cohort 析构// => 所有 Obj<B> 已 deletedestroy_resource_XYZ();

满足严格时序:
∼ B ( ) < d e s t r o y r e s o u r c e X Y Z ( ) \sim B() < destroy_resource_XYZ()B()<destroyresourceXYZ()
完全正确

九、总结对比(核心要点)

维度异步回收同步回收
C++26 标准
erase 返回后对象是否析构不保证保证
析构依赖外部资源危险安全
适用场景POD / 纯内存对象RAII / 有副作用析构

十、一句话总结(论文式)

Hazard Pointer 的异步回收足以保证内存安全,但无法保证析构时序;当析构函数依赖独立生命周期资源时,必须引入基于 cohort 的同步回收语义。
如果你愿意,我可以下一步帮你做:

  • Hazard Pointer vs Epoch / RCU 的同步能力对比
  • 为什么标准先只给异步(委员会角度)
  • 用 Rust / C++ RAII 的视角重新解释 cohort

// hazard_pointer_cohort:Hazard Pointer 的“同步回收分组”对象//// 语义核心:// - cohort 表示一个“回收边界(reclamation boundary)”// - 所有被 retire_to_cohort() 绑定到该 cohort 的对象// 必须在 cohort 析构之前被真正 delete//// 作用:// - 将 Hazard Pointer 从“最终会回收(asynchronous)”// 提升为“在此之前必须回收(synchronous)”classhazard_pointer_cohort{// 构造一个 cohort//// 构造完成后:// - 可以向该 cohort 绑定 retired 对象// - cohort 本身通常作为一个作用域对象存在hazard_pointer_cohort()noexcept;// 禁止拷贝构造//// 原因:// - cohort 表示一个严格的生命周期边界// - 拷贝会导致“一个回收边界对应多个实体”// - 会破坏以下不变量:// $$ \text{~cohort} \Rightarrow \text{所有对象已回收} $$hazard_pointer_cohort(consthazard_pointer_cohort&)=delete;// 禁止移动构造//// 原因同样是为了保证:// - cohort 的身份唯一// - cohort 的析构点明确且不可转移hazard_pointer_cohort(hazard_pointer_cohort&&)=delete;// 禁止拷贝赋值hazard_pointer_cohort&operator=(consthazard_pointer_cohort&)=delete;// 禁止移动赋值hazard_pointer_cohort&operator=(hazard_pointer_cohort&&)=delete;// cohort 的析构函数(同步回收的关键)//// 析构语义:// 1. 触发一次或多次 hazard pointer 扫描// 2. 等待所有线程释放对 cohort 中对象的保护// 3. 确保 cohort 中所有对象已被真正 delete//// 形式化保证:// $$ \forall obj \in cohort,\; \text{delete}(obj) \text{ 已发生} $$//// 也就是说:// - 当 ~hazard_pointer_cohort() 返回时// - 不再存在任何“延迟析构”的对象~hazard_pointer_cohort();};// hazard_pointer_obj_base:可被 Hazard Pointer 管理的对象基类//// T:实际对象类型// D:自定义删除器(默认使用 default_delete<T>)//// 提供 retire_to_cohort(),用于“同步回收”场景template<classT,classD=default_delete<T>>classhazard_pointer_obj_base{// 将对象退休(retire),并显式绑定到某个 cohort//// 语义:// - 对象从逻辑结构中移除// - 加入 hazard pointer 的 retired 列表// - 并归属于给定的 cohort//// 与 retire()(异步)最大的区别:// - retire():析构时间不确定// - retire_to_cohort():析构必须发生在 cohort 析构之前//// 数学化表达:// $$ \text{delete}(obj) \le \text{~cohort} $$//// noexcept 的原因:// - 回收路径通常在并发代码中// - 不允许异常破坏内存回收不变量voidretire_to_cohort(hazard_pointer_cohort&,D d=D())noexcept;};// 显式触发一次“异步回收”流程//// 作用:// - 扫描所有 hazard pointer// - 回收当前已满足条件的 retired 对象//// 使用场景:// - 后台线程周期性调用// - executor / 线程池任务// - cohort 析构前的辅助回收//// 注意:// - 该函数本身仍然是“异步回收”// - 只有结合 hazard_pointer_cohort 才能形成同步回收语义voidasynchronous_reclamation()noexcept;// 相关实现与参考资料://// - Facebook Folly 实现:// folly/synchronization/Hazptr.h//// - CPPCON 2021 演讲:// Hazard Pointer Synchronous Reclamation//// 这些内容构成了该 Possible API 的实践基础// Possible API(可能的标准 API 形态)//// - synchronous:// 使用 hazard_pointer_cohort + retire_to_cohort()// 提供“作用域内完成回收”的强保证//// - asynchronous:// 传统 Hazard Pointer 模型// 提供“最终会回收”的弱保证//// 两者并存,使 Hazard Pointer 既能高性能,// 又能安全处理析构依赖外部资源的复杂类型

Hazard Pointer Synchronous Reclamation(风险指针的同步回收)

背景动机

传统的Hazard Pointer(HP)机制只保证:

对象不会在仍被线程访问时被释放
并不保证对象何时被释放
因此,标准 Hazard Pointer 属于:
Asynchronous Reclamation(异步回收)
形式化地说:
retire ( o b j ) ⇒ ∃ t ≥ t 0 , delete ( o b j ) \text{retire}(obj) \Rightarrow \exists t \ge t_0, \text{delete}(obj)retire(obj)tt0,delete(obj)
即:
对象最终会被删除,但具体时间不确定

同步回收要解决的问题

在某些场景中,析构函数依赖外部资源的生命周期(例如线程池、全局状态、硬件资源等),这要求:
delete ( o b j ) ≤ 资源销毁时刻 \text{delete}(obj) \le \text{资源销毁时刻}delete(obj)资源销毁时刻
也就是说:

在离开某个作用域之前,必须确保对象已经被真正析构
这正是Hazard Pointer Synchronous Reclamation要解决的问题。

Cohorts(回收分组)的核心思想

Cohort(回收组)是同步回收的关键抽象:

把一批 retired 对象绑定到一个作用域,当该作用域结束时,强制完成这些对象的回收。
可以理解为:
“这些对象必须在我走之前全部安全 delete。”

hazard_pointer_cohort—— 同步回收的作用域对象

classhazard_pointer_cohort{hazard_pointer_cohort()noexcept;hazard_pointer_cohort(consthazard_pointer_cohort&)=delete;hazard_pointer_cohort(hazard_pointer_cohort&&)=delete;hazard_pointer_cohort&operator=(consthazard_pointer_cohort&)=delete;hazard_pointer_cohort&operator=(hazard_pointer_cohort&&)=delete;~hazard_pointer_cohort();};

语义说明

  • 构造一个回收分组(cohort)
  • 该 cohort 表示:
    • 所有“归属于该 cohort 的对象”
    • 必须在 cohort 析构前被安全回收

为什么禁止拷贝 / 移动?

hazard_pointer_cohort(consthazard_pointer_cohort&)=delete;hazard_pointer_cohort(hazard_pointer_cohort&&)=delete;

原因是:

  • cohort 表示一个严格的生命周期边界
  • 如果允许拷贝 / 移动,会破坏以下不变量:
    cohort 析构 ⇒ 该 cohort 中所有对象已 delete \text{cohort 析构} \Rightarrow \text{该 cohort 中所有对象已 delete}cohort析构cohort中所有对象已delete

析构函数的关键语义

~hazard_pointer_cohort();

析构时保证:

  1. 触发回收流程
  2. 等待所有 hazard pointer 解除保护
  3. 确保 cohort 中的所有对象已被真正析构
    形式化保证:
    ∀ o b j ∈ cohort , delete ( o b j ) 已发生 \forall obj \in \text{cohort},\quad \text{delete}(obj) \text{ 已发生}objcohort,delete(obj)已发生

hazard_pointer_obj_base::retire_to_cohort

template<classT,classD=default_delete<T>>classhazard_pointer_obj_base{voidretire_to_cohort(hazard_pointer_cohort&,D d=D())noexcept;};

语义解释

  • 将对象:
    • 标记为 retired
    • 绑定到指定的 cohort
  • 与普通retire()的区别在于:

方法回收时机
retire()不确定(异步)
retire_to_cohort()cohort 析构前(同步保证)

语义对比(数学化)

异步回收:
retire ( o b j ) ⇒ delete ( o b j ) eventually \text{retire}(obj) \Rightarrow \text{delete}(obj) \text{ eventually}retire(obj)delete(obj)eventually
同步回收(cohort):
retire_to_cohort ( o b j , c ) ⇒ delete ( o b j ) ≤ c \text{retire\_to\_cohort}(obj, c) \Rightarrow \text{delete}(obj) \le \text{~}cretire_to_cohort(obj,c)delete(obj)c

asynchronous_reclamation()

voidasynchronous_reclamation()noexcept;

作用说明

  • 显式触发一次:
    • hazard pointer 扫描
    • retired 对象回收
  • 常用于:
    • 后台线程
    • executor / 线程池
    • cohort 析构前的辅助清理

同步 vs 异步 API 的设计意图

Asynchronous Reclamation(现行 C++26)

  • 简单
  • 高吞吐
  • 析构时间不可预测
  • 不适合析构依赖外部资源的类型

Synchronous Reclamation(Cohorts)

  • 稍高开销
  • 有确定的回收点
  • 析构安全
  • 适合复杂系统资源管理

与 Folly 的关系

  • Facebook Folly 已有成熟实现:
    • folly/synchronization/Hazptr.h
  • CPPCON 2021 专题演讲:
    • Hazard Pointer Synchronous Reclamation
  • 该 API 设计基本是:
    • 对 Folly 经验的标准化抽象

总结一句话(可以直接用在结论页)

Hazard Pointer Cohorts 将“最终会回收”升级为“在此之前必须回收”,使 Hazard Pointer 能安全用于析构依赖外部资源的并发系统。
如果你愿意,下一步我可以:

  • 把你之前Container<B>的错误例子完整改写成 cohort 版本
  • 用一张时间轴图解释cohort 析构时发生了什么
  • 对比Hazard Pointer Cohort vs Epoch/RCU
    你只要点一个。

// =======================// Hazard Pointer 同步回收 vs C++26 仅异步回收// =======================//// 核心问题:// Hazard Pointer 的 retire() 只保证“最终会删除”,// 并不保证“在某个确定时间点之前已经删除”。//// 当析构函数依赖外部资源时,这个不确定性会变成严重问题。//// -----------------------// C++26:仅支持【异步回收】(Asynchronous Reclamation)// -----------------------template<classT>classContainer{// Obj 继承 hazard_pointer_obj_base// 说明该对象的生命周期由 Hazard Pointer 框架管理classObj:hazard_pointer_obj_base<Obj>{T data;// 真正存储的数据/* etc */// 其他成员(指针、索引等)};voidinsert(T data){// 分配新对象// 对象一旦被发布给并发读线程,就可能被 hazard pointer 保护Obj*obj=newObj(data);/* etc */// 将 obj 插入到无锁/并发容器中}voiderase(Args args){Obj*obj=find(args);// 查找要删除的对象/* Remove obj from container */// 逻辑删除:从容器结构中移除// 但此时可能仍有其他线程通过 hazard pointer 访问它obj->retire();// retire() 的含义:// 1. 告诉 Hazard Pointer 系统:这个对象“已经不再被容器拥有”// 2. 但并不立刻 delete// 3. 只有当【没有任何 hazard pointer 指向它】时,才允许 delete//// 关键点:// delete 的时间是【不确定的】,可能立刻,也可能很久以后}};classA{// A 的析构函数:// 不依赖任何外部资源// 不访问其他具有独立生命周期的对象//// 因此:// 即使析构被延迟执行,也不会造成逻辑错误或 UB~A();};{Container<A>container;container.insert(a);container.erase(a);}// 重要说明:// 这里 container 已经离开作用域,但://// Obj(A) 可能仍然没有被 delete// 因为:// - 可能仍有并发线程持有 hazard pointer// - retire() 只是“异步回收”//// 但这是 OK 的:// 因为 A 的析构函数不依赖任何外部资源//// 内存安全// 生命周期语义安全//// 结论:OK// -----------------------// 同步回收(Synchronous Reclamation)// -----------------------//// 目标:// 在“某个确定时间点之前”,强制保证对象已经被 delete//template<classT>classContainer{classObj:hazard_pointer_obj_base<Obj>{T data;/* etc */};// hazard_pointer_cohort:回收分组(回收边界)//// 语义:// 所有 retire_to_cohort(cohort_) 的对象,// 在 cohort_ 析构之前,必须已经被 deletehazard_pointer_cohort cohort_;voidinsert(T data){Obj*obj=newObj(data);/* etc */}voiderase(Args args){Obj*obj=find(args);/* Remove obj from container */obj->retire_to_cohort(cohort_);// retire_to_cohort 的含义:// 1. 对象进入 Hazard Pointer 的退休列表// 2. 同时“绑定”到 cohort_// 3. cohort_ 析构时会阻塞/等待:// 直到该对象真正被 deleteex_.submit([]{asynchronous_reclamation();});// 触发一次异步回收扫描:// - 加快回收进度// - 减少 cohort_ 析构时需要等待的时间//// 注意:// 真正的“同步保证”不是来自这里,// 而是来自 cohort_ 的析构语义}};classB{// B 的析构函数:// 依赖外部资源 XYZ// 该资源拥有独立生命周期//// 如果析构发生在 XYZ 被销毁之后 → UB~B(){use_resource_XYZ();}};make_resource_XYZ();// 创建外部资源 XYZ{Container<B>container;container.insert(b);container.erase(b);}// ← container 离开作用域// 触发成员 cohort_ 的析构// cohort_ 析构时保证:// 所有 retire_to_cohort(cohort_) 的 Obj(B)// 已经:// 没有 hazard pointer 引用// 已经被 delete//// 即:// $$ \forall obj \in cohort,\quad delete(obj) < destroy\_resource\_XYZ $$destroy_resource_XYZ();// 现在销毁资源是安全的// 析构顺序正确// 无 use-after-free// 无 Undefined Behavior//// 结论:OK

一、背景核心问题

Hazard Pointer 的本质目标是:

在无锁并发结构中,保证对象在“仍可能被其他线程访问”时不会被释放。
但是传统 Hazard Pointer(也是C++26 目前仅有的形式)只保证:
对象最终会被删除(eventually deleted) \text{对象最终会被删除(eventually deleted)}对象最终会被删除(eventually deleted
不保证“在某个确定时间点之前一定被删除”

二、第一段:C++26 仅支持的【异步回收(Asynchronous Reclamation)】

template<classT>classContainer{classObj:hazard_pointer_obj_base<Obj>{T data;/* etc */};voidinsert(T data){Obj*obj=newObj(data);/* etc */}voiderase(Args args){Obj*obj=find(args);/* Remove obj from container */obj->retire();}};classA{// Deleter does not depend on resources// with independent lifetime.~A();};{Container<A>container;container.insert(a);container.erase(a);}// Obj containing 'a' may be not deleted yet.

1⃣ 这里发生了什么?

  • Obj继承自hazard_pointer_obj_base
  • erase()中调用obj->retire()
  • 这意味着:
    • 对象逻辑上已从容器移除
    • 物理释放(delete)被推迟

2⃣retire()的语义

retire()表示:
该对象加入“待回收列表”,等所有 hazard pointer 释放后再 delete \text{该对象加入“待回收列表”,等所有 hazard pointer 释放后再 delete}该对象加入待回收列表,等所有hazard pointer释放后再delete
也就是说:

  • 其他线程仍持有 hazard pointer指向该对象
  • 删除会被延迟
  • 删除发生时间不可预测

3⃣ 为什么对class A是 OK 的?

classA{~A();};

A的析构函数:

  • 不依赖任何外部资源
  • 不访问:
    • 已释放内存
    • 已销毁的全局/线程资源
  • 即使析构被延迟:
    • 语义仍然正确
    • 不会触发 UB
      因此结论是:

对象什么时候被删都无所谓 → OK

三、第二段:引入【同步回收(Synchronous Reclamation)】

template<classT>classContainer{classObj:hazard_pointer_obj_base<Obj>{T data;/* etc */};hazard_pointer_cohort cohort_;voidinsert(T data){Obj*obj=newObj(data);/* etc */}voiderase(Args args){Obj*obj=find(args);/* Remove obj from container */obj->retire_to_cohort(cohort_);ex_.submit([]{asynchronous_reclamation();});}};

1⃣ 新增的关键元素:hazard_pointer_cohort

cohort_表示一个回收边界(reclamation boundary)

所有 retire 到该 cohort 的对象,必须在 cohort 析构前被删除
形式化描述为:
∀ o b j ∈ c o h o r t , d e l e t e ( o b j ) ≤ cohort \forall obj \in cohort,\quad delete(obj) \le \text{~cohort}objcohort,delete(obj)cohort

2⃣retire_to_cohort(cohort_)的语义

它不是普通的retire(),而是:

  • 把对象加入 hazard pointer 的退休列表
  • 同时绑定到 cohort
  • cohort 析构时会:
    1. 等待所有 hazard pointer 释放
    2. 强制完成对象回收

3⃣asynchronous_reclamation()的作用

ex_.submit([]{asynchronous_reclamation();});

这一步:

  • 加速回收过程
  • 提前触发扫描
  • 减少 cohort 析构时的阻塞时间
    但注意:

真正的同步保证来自 cohort 的析构,而不是这个函数本身

四、为什么第二段必须使用同步回收?

classB{~B(){use_resource_XYZ();}};

1⃣ 问题本质

B的析构函数:

  • 依赖外部资源XYZ
  • 且该资源有独立生命周期
make_resource_XYZ();{Container<B>container;container.insert(b);container.erase(b);}destroy_resource_XYZ();

如果使用异步回收

  • erase()只调用retire()
  • 对象Obj(B)可能还没被 delete
  • destroy_resource_XYZ()已经发生
  • 析构~B()再访问XYZ
    结果:
    Use-after-destroy ⇒ Undefined Behavior \text{Use-after-destroy} \Rightarrow \text{Undefined Behavior}Use-after-destroyUndefined Behavior

五、同步回收如何解决?

引入 cohort 后:

hazard_pointer_cohort cohort_;

对象通过:

obj->retire_to_cohort(cohort_);

绑定到 cohort。
container离开作用域时:

  1. ~Container()被调用
  2. 成员cohort_析构
  3. cohort 保证所有绑定对象已 delete
    因此:
// Obj containing 'b' must be already deleted.destroy_resource_XYZ();

成立。

六、对比总结(核心结论)


模式回收保证是否可控适用析构
异步回收(C++26)最终回收不确定简单析构
同步回收(Cohort)析构前必回收强保证依赖外部资源

七、一句话结论

Hazard Pointer 的异步回收保证“内存安全”,而同步回收(Cohort)保证“生命周期安全”。

// ========================================// Hazard Pointer 批量创建与销毁(Batch Creation and Destruction)// ========================================//// 核心问题:// 在传统单个创建/销毁的方式下,每个 hazard_pointer// 都需要独立分配和初始化,销毁时也单独处理。// 并发性能上有一定开销(如 ~6 ns)。//// C++26 标准(一个一个创建):{hazard_pointer hp[3];// 声明三条 hazard pointer,但此时它们都是 empty 状态/* Three hazard pointers are made nonempty separately. */hp[0]=make_hazard_pointer();// 分别初始化第 0 个 hazard pointerhp[1]=make_hazard_pointer();// 分别初始化第 1 个 hazard pointerhp[2]=make_hazard_pointer();// 分别初始化第 2 个 hazard pointerassert(!hp[0].empty());// 验证第 0 个已非空assert(!hp[1].empty());// 验证第 1 个已非空assert(!hp[2].empty());// 验证第 2 个已非空/* src is atomic<T*> */T*ptr=hp[0].protect(src);// 通过 hp[0] 获取受保护的对象指针 ptr/* etc */}/* Three nonempty hazard pointers are destroyed separately. */// 退出作用域时,hp[0], hp[1], hp[2] 各自析构,分别清理// 性能:大约 ~6 ns(每条单独处理)// ========================================// 批量创建与销毁(Batch creation and destruction)// ========================================{hazard_pointer hp[3];/* Three hazard pointers are made nonempty together. */make_hazard_pointer_batch(std::span{hp});// 一次性将 hp 数组的三个 hazard pointer 初始化为非空// 相比单个 make_hazard_pointer,多条同时处理,// 内部可能使用批量分配或优化路径 → 性能更优SCOPE_EXIT{destroy_hazard_pointer_batch(std::span{hp});// 批量销毁 hp 数组的三个 hazard pointer// 内部可能一次性释放资源,提高效率};assert(!hp[0].empty());assert(!hp[1].empty());assert(!hp[2].empty());/* src is atomic<T*> */T*ptr=hp[0].protect(src);// 与单个使用方式相同/* etc */}/* Three nonempty hazard pointers are emptied together, and then destroyed separately. */// 批量清空,然后依次析构,性能大约 ~2 ns(明显低于单个创建/销毁)// ========================================// Possible API// ========================================voidmake_hazard_pointer_batch(std::span<hazard_pointer>);// 批量初始化一组 hazard pointer 为非空voiddestroy_hazard_pointer_batch(std::span<hazard_pointer>)noexcept;// 批量销毁一组 hazard pointer// ========================================// 总结// ========================================//// 1. 单个创建/销毁://// - 每个 hazard pointer 独立初始化、销毁// - 性能:较低 (~6 ns)// - 使用简单,但高并发场景下开销大//// 2. 批量创建/销毁://// - 一次性初始化多条 hazard pointer// - 内部可优化批量分配// - 销毁时也可批量处理// - 性能:高 (~2 ns)// - 高并发场景更适合//// 3. 使用场景:// - 当你需要频繁创建大量 hazard pointer 时,推荐批量 API// - 单个创建适合偶尔使用或数量较少的情况//

Hazard Pointer 批量创建与销毁理解

背景

  • Hazard Pointer(HP)是一种安全回收机制,用于在多线程环境下安全访问共享对象。
  • 当对象可能被其他线程删除时,HP 能保证访问期间对象不会被回收。
  • 问题:在高并发场景下,频繁创建和销毁 HP 会带来性能开销。
    • 单个创建/销毁时间大约为6 ns 6\text{ ns}6ns
    • 批量创建/销毁可降低到大约2 ns 2\text{ ns}2ns,提升 3 倍性能。

单个创建/销毁方式(C++26 默认)

{hazard_pointer hp[3];// 声明三条 HP,初始状态都是 emptyhp[0]=make_hazard_pointer();// 单独初始化第 0 个 HPhp[1]=make_hazard_pointer();// 单独初始化第 1 个 HPhp[2]=make_hazard_pointer();// 单独初始化第 2 个 HPassert(!hp[0].empty());// 确认第 0 个非空assert(!hp[1].empty());// 确认第 1 个非空assert(!hp[2].empty());// 确认第 2 个非空T*ptr=hp[0].protect(src);// 使用 HP 保护 atomic 对象 src}

理解

  1. 每个 HP 都单独分配、初始化和销毁。
  2. 创建和销毁都是独立操作,存在性能开销。
  3. 适合 HP 数量少、访问不频繁的场景。
  4. 高并发下开销明显,因为T ∗ p t r = h p [ i ] . p r o t e c t ( s r c ) T* ptr = hp[i].protect(src)Tptr=hp[i].protect(src)频繁调用时,每次都要操作 HP 内部数据结构。

批量创建/销毁方式(Batch Creation and Destruction)

{hazard_pointer hp[3];make_hazard_pointer_batch(std::span{hp});// 一次性批量创建 HPSCOPE_EXIT{// 离开作用域时批量销毁destroy_hazard_pointer_batch(std::span{hp});};assert(!hp[0].empty());assert(!hp[1].empty());assert(!hp[2].empty());T*ptr=hp[0].protect(src);// 使用 HP 保护 atomic 对象 src}

理解

  1. 批量创建:
    • 一次性初始化整个 HP 数组,内部可采用批量分配、优化链表等机制。
    • 减少每条 HP 的单独初始化开销。
    • 性能显著提高,约2 ns 2\text{ ns}2ns
  2. 批量销毁:
    • 将整个 HP 数组统一清空,再依次销毁。
    • 避免单条销毁带来的重复操作。
    • 对高并发系统的效率更友好。
  3. 使用场景:
    • 当需要大量 HP 或频繁访问 shared object 时,批量创建/销毁显著降低开销。
    • 单个 HP 适合偶尔访问的轻量级场景。

核心 API

void make_hazard_pointer_batch(std::span<hazard_pointer>); \text{void make\_hazard\_pointer\_batch(std::span<hazard\_pointer>);}void make_hazard_pointer_batch(std::span<hazard_pointer>);

  • 批量初始化 HP 数组为非空。
    void destroy_hazard_pointer_batch(std::span<hazard_pointer>) noexcept; \text{void destroy\_hazard\_pointer\_batch(std::span<hazard\_pointer>) noexcept;}void destroy_hazard_pointer_batch(std::span<hazard_pointer>) noexcept;
  • 批量销毁 HP 数组。

性能对比公式

设单个 HP 创建/销毁时间为t s t_sts,批量创建/销毁总时间为t b t_btb,HP 数量为n nn,则:

  • 单个方式总时间:
    T s = n ⋅ t s T_s = n \cdot t_sTs=nts
  • 批量方式总时间:
    T b ≈ t b ( 批量优化后的总耗时 ) T_b \approx t_b \quad (\text{批量优化后的总耗时})Tbtb(批量优化后的总耗时)
  • 性能提升比率:
    Speedup = T s T b ≈ 6 , ns × 3 2 , ns = 9 \text{Speedup} = \frac{T_s}{T_b} \approx \frac{6,\text{ns} \times 3}{2,\text{ns}} = 9Speedup=TbTs2,ns6,ns×3=9
    (在示例中 3 个 HP 的情况下,批量创建/销毁约提升 3 倍以上)

总结

  1. Hazard Pointer解决了多线程下对象访问的安全问题。
  2. 单个创建/销毁简单,但在高并发场景下性能差。
  3. 批量创建/销毁可以显著提升效率:
    • 初始化更快
    • 销毁更快
    • 内部实现可做批量优化
  4. 高并发系统或大量 HP 的场景,强烈推荐使用make_hazard_pointer_batchdestroy_hazard_pointer_batch

Pointer Tagging(指针标记)概念

来源:P3125R0,由 Hana Dusíková 提出 wg21.link/p3125r0。

1. 指针标记的定义

Pointer Tagging 是一种在指针本身的低位或高位空闲位存储额外信息的技术

  • 假设有一个对齐的对象T TT,其对齐要求为a l i g n o f ( T ) = 4 alignof(T) = 4alignof(T)=4,那么指针的低两位必定为0 00(因为 4 字节对齐的对象地址总是 4 的倍数)。
  • 利用这些低位可以存储一些额外信息,比如状态标志或小整数,而不改变指针指向的实际对象。
    示意(64 位指针,低两位可用作标记):
    Pointer to aligned object T : 0000000000000000 ? ? ? ? ? ? ? ? ? ? \text{Pointer to aligned object } T:\quad 0000000000000000??????????Pointer to aligned objectT:0000000000000000??????????
  • 高位部分用于存储实际地址。
  • 低位(未使用的对齐空位)用于存储自定义标记。

2. 动机(Motivation)

P3125R0 中提出指针标记的动机:

  1. C++ 标准目前不允许操作指针的位
    • 在标准 C++ 中直接操作指针的二进制位是未定义行为(UB, Undefined Behavior)
    • 因此,某些高级数据结构(如锁自由数据结构、压缩型链表、状态标记对象)无法安全或高效实现。
  2. 实践中已广泛使用
    • 在操作系统、并发算法、GC(垃圾回收)和高性能数据结构中,pointer tagging 是成熟技术。
    • 将其标准化可以降低 C++ 开发者使用此技术的门槛,提高安全性。
  3. 潜在收益
    • 可以在指针本身存储额外信息,而无需增加额外字段,从而减少内存占用。
    • 对于锁自由算法和并发数据结构,可以更高效地存储状态、标记或版本号。

3. 典型示例

假设 32 位系统,对齐为 4 的对象指针:

  • 实际地址示例(最低两位总是 00):
    ptr = 0 x 1000 \text{ptr} = 0x1000ptr=0x1000
  • 可以用低两位存储状态标志,例如:
    • 00: 正常
    • 01: 已删除
    • 10: 已访问
    • 11: 保留
  • 得到标记指针:
    tagged_ptr = ptr ∣ tag_bits \text{tagged\_ptr} = \text{ptr} | \text{tag\_bits}tagged_ptr=ptrtag_bits

4. 关键点总结

  1. 原理:利用对齐空闲位存储额外信息。
  2. 限制
    • 当前标准 C++ 下直接操作指针位是 UB。
    • 必须确保访问对象时清除标记位。
  3. 用途
    • 高性能数据结构(lock-free、wait-free)。
    • 低内存开销存储额外状态信息。
  4. 目标:将 pointer tagging 标准化,安全使用,提高可移植性。

1. 基本类型定义

template<typenameT,size_t Alignment=alignof(T)>classtagged_pointer;
  • tagged_pointer<T, Alignment>表示带标记的指针类型
  • 模板参数T是指针指向的类型,Alignment是对象对齐要求(默认alignof(T))。
  • 这个类型封装了原始指针以及存储在低位的 tag 信息。

2. 可用标记位掩码

template<typenameT,size_t Alignment=alignof(T)>constexprautotag_bit_mask()noexcept->uintptr_t;
  • 返回当前类型T对齐要求下,可用于存储 tag 的位掩码
  • 对齐保证了低n nn位总是0 00,可以安全使用它们存储 tag。
  • 示例:如果a l i g n o f ( T ) = 4 alignof(T) = 4alignof(T)=4,低两位可用,则:
    tag_bit_mask<T,4>() = 0 b 11 \text{tag\_bit\_mask<T,4>()} = 0b11tag_bit_mask<T,4>()=0b11

3. 将普通指针打上 tag

template<typenameT,size_t Alignment=alignof(T)>constexprautotag_pointer(T*original,uintptr_t tag)noexcept->tagged_pointer<T,Alignment>;
  • 功能:将原始指针与 tag 位组合生成tagged_pointer
  • 先决条件:
    t a g = = ( t a g & t a g _ b i t _ m a s k < T , A l i g n m e n t > ( ) ) tag == (tag \& tag\_bit\_mask<T, Alignment>())tag==(tag&tag_bit_mask<T,Alignment>())
    • 意味着 tag 只能占用允许的空闲低位,避免覆盖指针地址本身。
  • 返回值类型:tagged_pointer<T, Alignment>,即带标记的指针对象。

4. 从tagged_pointer获取原始指针

template<typenameT,size_t Alignment=alignof(T)>constexprautountag_pointer(tagged_pointer<T,Alignment>ptr)noexcept->T*;
  • 功能:提取tagged_pointer中存储的原始指针,忽略 tag 位。
  • 实现上会用掩码清除低位标记:
    original_ptr = p t r & ∼ t a g _ b i t _ m a s k < T , A l i g n m e n t > ( ) \text{original\_ptr} = ptr \& \sim tag\_bit\_mask<T, Alignment>()original_ptr=ptr&tag_bit_mask<T,Alignment>()

5. 获取tagged_pointer中的 tag 值

template<typenameT,size_t Alignment=alignof(T)>constexprautotag_value(tagged_pointer<T,Alignment>ptr)noexcept->uintptr_t;
  • 功能:tagged_pointer中提取存储的 tag 信息
  • 实现上用掩码提取低位:
    tag = p t r & t a g _ b i t _ m a s k < T , A l i g n m e n t > ( ) \text{tag} = ptr \& tag\_bit\_mask<T, Alignment>()tag=ptr&tag_bit_mask<T,Alignment>()

6. 使用示意

假设有一个指针T* ptr,对齐为 4:

autotagged=tag_pointer(ptr,0b01);// 在低两位存储标记 01T*original=untag_pointer(tagged);// 取回原指针uintptr_t tag=tag_value(tagged);// 取回标记值 01
  • 优点:不增加额外内存,直接在指针本身存储状态信息
  • 对齐保证安全性:低位空闲位不会破坏指针有效性。
  • 可用于 lock-free 数据结构、状态标记、版本号等场景。

总结

  1. tagged_pointer:封装指针 + 标记。
  2. tag_bit_mask():返回可用的低位掩码。
  3. tag_pointer():生成带标记的指针。
  4. untag_pointer():获取原始指针。
  5. tag_value():获取存储在指针中的标记。
  • 公式关键点:
    tagged_ptr = original_ptr ∣ t a g \text{tagged\_ptr} = \text{original\_ptr} | tagtagged_ptr=original_ptrtag
    original_ptr = t a g g e d _ p t r & ∼ t a g _ b i t _ m a s k \text{original\_ptr} = tagged\_ptr \& \sim tag\_bit\_maskoriginal_ptr=tagged_ptr&tag_bit_mask
    t a g = t a g g e d _ p t r & t a g _ b i t _ m a s k tag = tagged\_ptr \& tag\_bit\_masktag=tagged_ptr&tag_bit_mask

1. 指针打标记示例

usingTaggedPointer=tagged_pointer<T,2>;booltry_tag_untagged_pointer(atomic<TaggedPointer>&src){TaggedPointer current=src.load();assert(tag_value(current)==0);// 确认当前指针未打标记assert(1&tag_bit_mask<T,2>()==1);// 确认 1 是合法的标记位T*ptr=untag_pointer(current);// 提取原始指针TaggedPointer newval=tag_pointer(ptr,1);// 为指针打标记 1returnsrc.compare_exchange_weak(current,newval);// CAS 操作更新原子指针}

理解

  1. TaggedPointer current = src.load();
    • 从原子变量src中读取当前值(可能未打标记)。
  2. tag_value(current) == 0
    • 断言当前指针的 tag 值为 0,即未打标记。
  3. 1 & tag_bit_mask<T,2>() == 1
    • 断言 tag 位1是合法的低位可用空间,安全存储在指针低两位。
  4. T* ptr = untag_pointer(current);
    • 提取原始指针,忽略低位标记。
  5. TaggedPointer newval = tag_pointer(ptr, 1);
    • 将 tag 1 写入指针低位,生成新的带标记指针。
  6. compare_exchange_weak(current, newval)
    • 使用 CAS(Compare-And-Swap)安全更新原子指针,避免数据竞争。
  • 公式表示:
    newval = original_ptr ; ∣ ; tag \text{newval} = \text{original\_ptr} ;|; \text{tag}newval=original_ptr;;tag
    original_ptr = untag_pointer(newval) \text{original\_ptr} = \text{untag\_pointer(newval)}original_ptr=untag_pointer(newval)
    tag = tag_value(newval) \text{tag} = \text{tag\_value(newval)}tag=tag_value(newval)

2. Hazard Pointer 扩展到 Tagged Pointer

C++26 标准原生的 Hazard Pointer API:

template<typenameT>T*protect(constatomic<T*>&src)noexcept;template<typenameT>booltry_protect(T*&ptr,constatomic<T*>&src)noexcept;
  • 保护原子指针,防止在多线程环境中被释放。
    扩展到 tagged_pointer:
template<typenameT,size_t Alignment=alignof(T)>tagged_pointer<T,Alignment>protect(constatomic<tagged_pointer<T,Alignment>>&src)noexcept;template<typenameT,size_t Alignment=alignof(T)>booltry_protect(tagged_pointer<T,Alignment>&ptr,constatomic<tagged_pointer<T,Alignment>>&src)noexcept;
  • 功能与普通 Hazard Pointer 类似,但操作的是带标记指针
  • 可以安全保护指针及其 tag,保证多线程访问时不会出现悬空或未同步状态。

3. 使用示例

atomic<tagged_pointer<T>>src_;hazard_pointer hp=make_hazard_pointer();tagged_pointer<T>tagged=hp.protect(src_);/* 此时可以安全使用 ptr,其中 ptr == untag_pointer(tagged) */

理解

  1. atomic<tagged_pointer<T>> src_;
    • 原子变量,存储带标记的指针。
  2. hazard_pointer hp = make_hazard_pointer();
    • 创建一个 hazard pointer,用于保护指针。
  3. tagged_pointer<T> tagged = hp.protect(src_);
    • 将原子变量中的指针和 tag 一起保护起来,防止在访问期间被删除或修改。
  4. ptr == untag_pointer(tagged)
    • 可以安全地访问原始指针,忽略低位 tag,避免悬空访问。

总结

  • Pointer Tagging:在指针低位存储额外信息(tag),节省内存。
  • Hazard Pointer 扩展:可保护带标记指针,保证多线程安全访问。
  • CAS 与 protect 操作确保了无锁并发安全
  • 公式关键点:
    tagged_ptr = original_ptr ∣ tag \text{tagged\_ptr} = \text{original\_ptr} | \text{tag}tagged_ptr=original_ptrtag
    original_ptr = untag_pointer(tagged_ptr) \text{original\_ptr} = \text{untag\_pointer(tagged\_ptr)}original_ptr=untag_pointer(tagged_ptr)
    tag = tag_value(tagged_ptr) \text{tag} = \text{tag\_value(tagged\_ptr)}tag=tag_value(tagged_ptr)

1. 引入背景

  • 目标:将并行性 (parallelism) 引入std::ranges算法,使算法在多核/多线程环境下高效执行。
  • 来源:基于 ISO C++ 的并行/并发编程语言扩展提案,以及 Gonzalo 的 ISC C++ BoF 讨论。

2. 历史演进

C++ 2017

  • Parallel Algorithms:支持许多并行算法。
  • Concurrency++:并发库增强,改进线程管理、同步机制。
  • Memory Model++:改进内存模型,确保多线程程序的正确性(贡献者 MichaelW、Maged、Paul M)。
  • Forward Progress:确保程序在多线程环境下不会无限阻塞。

C++ 2020

  • Concepts:提供模板约束,使泛型编程更安全。
  • Ranges:范围库,简化算法与容器的组合。
  • Modules:模块化机制,提高编译速度与封装性。
  • Concurrency++:继续扩展并发能力(贡献者 Bryce, Gonzalo)。
  • Coroutines:协程,支持异步任务和延迟计算。
  • atomic_ref, barriers, …:引入新的原子操作和同步屏障。

C++ 2023

  • Ranges++:进一步丰富范围库功能。
  • Multi-dimensional Spans:支持多维数据视图 (span)。
  • operator[i, j, k]:提供多维索引访问的操作符,方便矩阵/张量操作。

C++ 2026(规划中)

  • Executors / Sender / Receiver [P2300]:异步任务调度和执行模型。
  • Reflection:提供运行时和编译时反射能力。
  • Algorithms++:范围算法扩展,引入异步支持(贡献者 Ruslan, Alexey, Bryce)。
  • Linear Algebra, submdspan, padded layouts:线性代数库扩展,多维子视图、带填充布局(贡献者 Bryce, Christian)。
  • Concurrency++ RCU, HP:高级并发支持,包括 RCU(Read-Copy-Update)和 Hazard Pointer(HP)(贡献者 MichaelW, Gonzalo, Maged, Paul M)。
  • SIMD:单指令多数据向量化操作(贡献者 Matthias, Daniel, Ruslan)。

3. 理解总结

  1. 发展趋势
    • 从 C++17 开始支持基本并行算法,逐步引入范围库、协程和多维数据结构。
    • 到 C++2026,将实现异步范围算法、执行器、反射、高级并发与 SIMD
  2. 目标
    • 提升算法在并行和多核环境下的性能。
    • 简化多线程和异步编程模型。
    • 支持高效线性代数计算和多维数据操作。
  3. 关键概念
  • Ranges:使算法可以直接作用于“范围”,简化迭代器操作。
  • Executors / Sender / Receiver:提供统一的异步任务调度机制。
  • Hazard Pointer (HP):安全管理并发访问的动态内存对象。
  • RCU (Read-Copy-Update):允许多线程读写共享数据的高效策略。
  • SIMD:通过单指令操作多数据,提高向量计算性能。
  1. 公式化理解
  • 假设一个范围算法alg在序列seq上运行:
    result = alg ( seq ) \text{result} = \text{alg}(\text{seq})result=alg(seq)
  • 引入并行执行策略后:
    result = alg ( seq , execution_policy ) \text{result} = \text{alg}(\text{seq}, \text{execution\_policy})result=alg(seq,execution_policy)
  • 异步算法可表示为:
    future<result> = alg.async ( seq ) \text{future<result>} = \text{alg.async}(\text{seq})future<result>=alg.async(seq)
  • 多维 span 支持多维索引访问:
    value = s p a n [ i , j , k ] \text{value} = span[i,j,k]value=span[i,j,k]

1. 背景与发展

历史对比

  1. C++03(经典 STL 算法)
template<classRandomAccessIterator,classCompare>voidsort(RandomAccessIterator first,RandomAccessIterator last,Compare comp);
  • 基于迭代器的排序,只支持顺序执行。
  • 算法接口简单,但无法直接控制并行执行。
  1. C++17(引入执行策略)
template<classExecutionPolicy,classRandomAccessIterator,classCompare>voidsort(ExecutionPolicy&&exec,RandomAccessIterator first,RandomAccessIterator last,Compare comp);
  • 支持执行策略(ExecutionPolicy),可以选择并行(std::execution::par)或顺序(std::execution::seq)。
  • 仍然基于迭代器,没有直接与范围(ranges)结合。
  1. C++20(范围库支持)
template<random_access_range R,classComp=ranges::less,classProj=identity>requiressortable<iterator_t<R>,Comp,Proj>constexprborrowed_iterator_t<R>ranges::sort(R&&r,Comp comp={},Proj proj={});
  • 引入ranges,用户可以直接操作范围对象而非迭代器。
  • 算法接口更加表达式化,支持惰性组合(lazy composition)。
  • 默认比较函数为ranges::less,并支持投影(projection)函数Proj
  1. C++26(未来提案 P3179)
template<classExecutionPolicy,random_access_range R,classComp=ranges::less,classProj=identity>requiressortable<iterator_t<R>,Comp,Proj>constexprborrowed_iterator_t<R>ranges::sort(ExecutionPolicy&&exec,R&&r,Comp comp={},Proj proj={});
  • 在范围算法中引入执行策略,统一并行与范围 API。
  • 用户可以直接在范围上使用并行算法:
    ranges::sort(exec_policy, my_range) \text{ranges::sort(exec\_policy, my\_range)}ranges::sort(exec_policy, my_range)

2. 为什么将并行性与范围结合?

  • Ranges 提供生产力
    • Ranges API 易读、可组合、表达能力强。
  • 并行算法普遍存在
    • 用户常常在范围上使用非范围并行算法,但接口不统一、易出错。
  • 统一优势
    • 将执行策略直接整合到范围算法中,可简化代码、降低错误率。

3. 范围 + 并行的优势

  1. 表达能力强
    • Ranges 支持惰性计算(lazy evaluation),可组合多个操作。
  2. 性能优化机会
    • 并行范围算法可自动利用多核 CPU 或 GPU,提高性能。
  3. 自然语义
    • 避免将迭代器与执行策略分开管理,API 更直观。
  4. 易于迁移
    • 返回类型与序列范围算法一致,旧代码迁移简单。

4. 并行范围算法设计要点

  1. 执行策略参数
    • 所有范围算法可接受ExecutionPolicy,如:
      std::execution::seq , std::execution::par , std::execution::par_unseq \text{std::execution::seq}, \text{std::execution::par}, \text{std::execution::par\_unseq}std::execution::seq,std::execution::par,std::execution::par_unseq
  2. 随机访问范围
    • 并行化算法要求输入范围是随机访问的:
      R ∈ random_access_range R \in \text{random\_access\_range}Rrandom_access_range
  • 保证可以高效地分块和分配任务。
  1. 边界范围 (Bounded Range)
    • 至少一个输入和输出范围必须是有界的(bounded),保证安全和性能。
  2. 一致的返回类型
    • 保持与序列范围算法一致,便于单步替换:
      borrowed_iterator_t<R> \text{borrowed\_iterator\_t<R>}borrowed_iterator_t<R>
  3. 多操作单次调用融合
    • 支持将多个操作组合成单次并行调用,减少调度开销。
  4. 保持表达力
    • 保留 ranges API 的可组合性和易读性。

5. 总结理解

  • C++17:并行算法 + 迭代器接口。
  • C++20:范围算法,增强表达力。
  • C++26:并行范围算法,将执行策略直接整合到范围算法中,实现统一、高效、易读的并行计算 API。
  • 用户可以写出:
    ranges::sort(std::execution::par, my_vector) \text{ranges::sort(std::execution::par, my\_vector)}ranges::sort(std::execution::par, my_vector)
  • 同时保持与非并行范围算法的兼容性,并简化多操作组合与优化。

1. 关键设计决策(Key Design Decisions)

  1. 返回类型与序列范围算法一致
    • 并行ranges::for_each返回类型为:
      ranges::borrowed_iterator_t<R> \text{ranges::borrowed\_iterator\_t<R>}ranges::borrowed_iterator_t<R>
    • 这样可以保证与顺序版本兼容,便于旧代码迁移和替换。
  2. 要求输入范围为随机访问范围 (random_access_range)
    • 目前为了高效并行化,算法要求R ∈ random_access_range R \in \text{random\_access\_range}Rrandom_access_range
    • 随机访问保证可以快速分块、分配任务,降低线程间同步开销。
  3. 输出范围为输入参数
    • 算法不仅遍历输入范围,还可以直接在输出范围上操作(in-place 或覆盖)。
  4. 要求边界范围 (bounded ranges)
    • 至少一个输入和输出范围必须是有界的(sized/bounded),保证安全性和高效性:
      sized_sentinel_for<sentinel_t<R>, iterator_t<R>> \text{sized\_sentinel\_for<sentinel\_t<R>, iterator\_t<R>>}sized_sentinel_for<sentinel_t<R>, iterator_t<R>>
    • 便于计算块大小和任务划分。
  5. 保持 C++17 并行算法可调用要求
    • 保持 callable 函数对象(functor)满足indirectly_unary_invocable要求:
      indirectly_unary_invocable<projected<iterator_t<R>, Proj>, Fun> \text{indirectly\_unary\_invocable<projected<iterator\_t<R>, Proj>, Fun>}indirectly_unary_invocable<projected<iterator_t<R>, Proj>, Fun>
    • 保证算法在并行执行时函数对象仍然可调用且安全。

2. 并行ranges::for_each模板接口解释

template<classExecutionPolicy,random_access_range R,classProj=identity,indirectly_unary_invocable<projected<iterator_t<R>,Proj>>Fun>requiressized_sentinel_for<ranges::sentinel_t<R>,ranges::iterator_t<R>>ranges::borrowed_iterator_t<R>ranges::for_each(ExecutionPolicy&&policy,R&&r,Fun f,Proj proj={});

逐行理解

  1. 模板参数
    • ExecutionPolicy:执行策略,可选顺序或并行,例如:
      std::execution::seq , std::execution::par , std::execution::par_unseq \text{std::execution::seq}, \text{std::execution::par}, \text{std::execution::par\_unseq}std::execution::seq,std::execution::par,std::execution::par_unseq
    • random_access_range R:输入范围,必须是随机访问范围。
    • Proj = identity:投影函数,可将元素映射到某个属性。
    • Fun:函数对象,必须满足:
      indirectly_unary_invocable<projected<iterator_t<R>, Proj>, Fun> \text{indirectly\_unary\_invocable<projected<iterator\_t<R>, Proj>, Fun>}indirectly_unary_invocable<projected<iterator_t<R>, Proj>, Fun>
  2. 约束条件 (requires)
    • 输入范围必须是有界的(sized/bounded):
      sized_sentinel_for<ranges::sentinel_t<R>, ranges::iterator_t<R>> \text{sized\_sentinel\_for<ranges::sentinel\_t<R>, ranges::iterator\_t<R>>}sized_sentinel_for<ranges::sentinel_t<R>, ranges::iterator_t<R>>
  3. 返回类型
    • 与序列范围算法一致,返回借用迭代器:
      ranges::borrowed_iterator_t<R> \text{ranges::borrowed\_iterator\_t<R>}ranges::borrowed_iterator_t<R>
  4. 函数参数
    • policy:执行策略。
    • r:输入范围。
    • f:函数对象,对每个元素执行操作。
    • proj:投影函数(默认identity)。

3. 总结理解

  • C++26 并行范围算法将执行策略范围 API统一。
  • 输入范围必须是随机访问 + 有界,以保证高效并行化。
  • 函数对象必须可调用(indirectly_unary_invocable),保持并行安全。
  • 返回类型保持与序列范围算法一致,便于迁移和组合操作。
  • 投影函数允许用户在遍历时提取元素属性,提高灵活性。

1. 与 C++17 并行算法的关键差异(Key Differences)

  1. 输入范围要求随机访问(random access ranges)
    • C++17 并行算法可以接受更广泛的迭代器类型,但 C++26 并行范围算法要求:
      R ∈ random_access_range R \in \text{random\_access\_range}Rrandom_access_range
    • 这样保证算法能够高效地进行分块并行计算,减少线程间同步开销。
  2. 输出可以是一个范围而不仅仅是迭代器
    • 在 C++17 中,大多数并行算法只返回一个迭代器,表示处理完毕的位置。
    • C++26 并行范围算法允许直接传入输出范围,返回一个范围或借用迭代器:
      return type = ranges::borrowed_iterator_t<R> 或输出范围 \text{return type} = \text{ranges::borrowed\_iterator\_t<R>} \text{ 或输出范围}return type=ranges::borrowed_iterator_t<R>或输出范围
    • 这简化了结果的处理,减少了手动迭代和赋值操作。

2. 优势(Benefits)

  1. 自然高效的范围并行化
    • 直接在范围上操作,结合执行策略(ExecutionPolicy)即可并行化:
      for_each(exec_policy, my_range, func) \text{for\_each(exec\_policy, my\_range, func)}for_each(exec_policy, my_range, func)
    • 避免了 C++17 中手动从迭代器获取子区间并分发任务的繁琐操作。
  2. 无缝集成 Ranges 库与现有并行算法
    • 保持范围的惰性组合能力,同时继承 C++17 并行算法的执行策略。
    • 用户可以轻松地将串行范围算法迁移到并行版本,无需重写大量代码。
  3. 代码更具表达力
    • 使用范围直接表示输入/输出集合,减少模板嵌套与迭代器类型声明。
    • 可读性和可维护性更高:
      auto result = ranges::sort(exec_policy, my_range); \text{auto result = ranges::sort(exec\_policy, my\_range);}auto result = ranges::sort(exec_policy, my_range);
  4. 潜在更高性能
    • 随机访问 + 边界范围 + 执行策略组合,允许编译器和运行时进行高效分块和负载均衡。
  5. 更安全的 API
    • 要求有界范围(bounded ranges)保证在并行执行中不会越界或访问无效内存。
    • 输出范围避免了返回裸迭代器可能带来的悬空问题。
  6. 简化串行到并行迁移
    • 串行范围算法与并行范围算法接口保持一致,只需添加执行策略:
      ranges::for_each(my_range, func) → ranges::for_each(exec_policy, my_range, func) \text{ranges::for\_each(my\_range, func)} \to \text{ranges::for\_each(exec\_policy, my\_range, func)}ranges::for_each(my_range, func)ranges::for_each(exec_policy, my_range, func)
    • 降低迁移复杂度,同时获得并行性能。
      总结:
      C++26 并行范围算法通过随机访问范围 + 输出范围 + 执行策略的组合,提供了更自然、更安全、更高效的并行计算方式,弥合了范围表达能力与并行执行能力的差距。

1. 背景与动机(Overview & Motivation)

  1. C++ 并行算法的发展演化
    • C++11/14 引入了基础的并行算法支持(std::executionstd::for_each等)。
    • C++17 引入了并行执行策略(std::execution::parseq),但无法指定执行硬件
    • C++26 借助 P2300 引入了可调度的执行器(scheduler),实现 “在哪里执行” 的灵活控制。
  2. 为什么需要 P2300
    • 现有执行策略只回答 “如何执行”(how),无法回答 “在哪里执行”(where)。
    • P2300 提供scheduler抽象,表示硬件执行上下文,例如 CPU 核心、GPU、异构设备等。
    • 将并行算法与 scheduler 集成,可以更有效利用硬件能力,提高性能和可扩展性。

2. P2500 的设计概览(Design Overview)

  1. 策略感知调度器(policy-aware scheduler)
    • 结合了执行策略(execution policy)调度器(scheduler)
      policy-aware scheduler = ( scheduler, execution policy ) \text{policy-aware scheduler} = (\text{scheduler, execution policy})policy-aware scheduler=(scheduler, execution policy)
    • 允许用户通过execute_on创建策略感知调度器:
      auto sched = s t d : : e x e c u t e o n ( m y s c h e d u l e r , s t d : : e x e c u t i o n : : p a r ) ; \text{auto sched} = std::execute_on(my_scheduler, std::execution::par);auto sched=std::executeon(myscheduler,std::execution::par);
  2. 设计目标(Design Goals)
    • 扩展 C++ 并行算法以支持策略感知调度器。
    • 保持算法和策略的核心语义不变。
    • 支持传统算法与基于范围的算法(range-based algorithms)。
    • 最小化 API 改动,保持向后兼容。
    • 允许根据硬件能力自定义算法实现,实现性能优化。
  3. 关键特性(Key Features)
    • 调度器与策略组合(scheduler + execution policy)。
    • 可扩展 API:允许为特定调度器自定义并行算法。
    • 保持阻塞行为与 C++17 并行算法一致。

3. 核心概念(Key Concepts)

  1. policy_aware_scheduler 概念
    template<typenameS>conceptpolicy_aware_scheduler=scheduler<S>&&requires(S s){typenameS::base_scheduler_type;typenameS::policy_type;{s.get_policy()}->execution_policy;};
    • 调度器必须提供base_scheduler_typepolicy_type
    • get_policy()返回关联的执行策略。
  2. execute_on 函数
    inlineconstexpr__detail::__execute_on_fn execute_on;
    • 将调度器与执行策略绑定,生成策略感知调度器:
      policy_aware_sched = std::execute_on(my_scheduler, std::execution::par) \text{policy\_aware\_sched} = \text{std::execute\_on(my\_scheduler, std::execution::par)}policy_aware_sched=std::execute_on(my_scheduler, std::execution::par)
  3. 并行算法自定义化
    • 并行算法定义为可自定义函数(customizable function),可以针对特定调度器实现优化版本(例如 CUDA 优化):
      namespacecuda{structscheduler{friendconstexprautotag_invoke(std::tag_t<ranges::for_each>,scheduler,/*...*/){cuda_kernel<<<blocks,threads>>>(/*...*/);returnstd::ranges::for_each_result{/*...*/};}};}

4. 使用示例(Usage Example)

std::for_each(std::execute_on(my_gpu_scheduler,std::execution::par),begin(data),end(data),[](auto&item){item.process();});
  • execute_on生成策略感知调度器,for_each在指定硬件上并行执行。
  • 保留了 C++17 并行算法的阻塞语义,同时支持硬件定制化。

5. API 对比

  1. 现有 C++17 API
template<classExecutionPolicy,classIt,classFun>constexprvoidfor_each(ExecutionPolicy&&policy,It first,It last,Fun f);
  1. 新策略 API(基于 execution_policy)
template<execution_policy Policy,input_iterator I,sentinel_for<I>S,classProj=identity,indirectly_unary_invocable<projected<I,Proj>>Fun>constexprranges::for_each_result<I,Fun>ranges::for_each(Policy&&policy,I first,S last,Fun f,Proj proj={});
  1. 调度器 API(policy-aware scheduler)
template<policy_aware_scheduler Scheduler,input_iterator I,sentinel_for<I>S,classProj=identity,indirectly_unary_invocable<projected<I,Proj>>Fun>constexprranges::for_each_result<I,Fun>ranges::for_each(Scheduler sched,I first,S last,Fun f,Proj proj={})/*customizable*/;
  • 支持针对不同硬件和执行上下文自定义算法实现。

6. 总结(Summary)

  • P2300:提供了灵活的硬件执行上下文抽象(scheduler),回答 “代码在哪里执行”。
  • P2500:将 scheduler 集成到 C++ 并行算法中,实现 “策略感知并行算法”,回答 “如何执行 + 在哪里执行”。
  • 未来方向:可定制、可扩展的并行算法,将进一步提升 C++ 并行编程的性能、表达力和安全性。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/1 11:13:54

ParsecVDD虚拟显示器终极攻略:从零开始打造多屏工作空间

ParsecVDD虚拟显示器终极攻略&#xff1a;从零开始打造多屏工作空间 【免费下载链接】parsec-vdd ✨ Virtual super display, upto 4K 2160p240hz &#x1f60e; 项目地址: https://gitcode.com/gh_mirrors/pa/parsec-vdd 还在为单屏幕限制工作效率而苦恼吗&#xff1f;…

作者头像 李华
网站建设 2026/5/5 9:02:56

JavaWeb之过滤器Filter监听器

Filter&#xff1a;过滤器&#xff0c;用来过滤网站的数据&#xff1b; 处理中文乱码登陆验证…… Fitler开发步骤 添加依赖 pom.xml <dependencies><!--Jsp依赖--><!-- https://mvnrepository.com/artifact/javax.servlet.jsp/javax.servlet.jsp-api -->…

作者头像 李华
网站建设 2026/5/3 8:40:41

Bypass Paywalls Clean:突破内容付费墙的完整指南

Bypass Paywalls Clean&#xff1a;突破内容付费墙的完整指南 【免费下载链接】bypass-paywalls-chrome-clean 项目地址: https://gitcode.com/GitHub_Trending/by/bypass-paywalls-chrome-clean 在当今数字信息时代&#xff0c;优质内容往往被付费墙所限制&#xff0c…

作者头像 李华
网站建设 2026/5/1 19:10:37

【瑞萨RA × Zephyr评测】SPI 屏 (SSD1306) + 双路 ADC

Zephyr 开发笔记&#xff1a;RA6E2 SPI 屏 (SSD1306) 双路 ADC 1. 硬件接线表 (根据代码分析) 根据你的 app.overlay 配置&#xff0c;硬件连接如下。请务必检查物理连线是否一致&#xff1a;模块功能 (Pin Name)RA6E2 引脚备注SSD1306D0 (SCK/CLK)P111SPI0 RSPCK (硬件 SPI 时…

作者头像 李华
网站建设 2026/5/5 5:29:06

Windows右键菜单终极管理工具:快速打造个性化桌面环境

Windows右键菜单终极管理工具&#xff1a;快速打造个性化桌面环境 【免费下载链接】ContextMenuManager &#x1f5b1;️ 纯粹的Windows右键菜单管理程序 项目地址: https://gitcode.com/gh_mirrors/co/ContextMenuManager 在日常电脑使用中&#xff0c;右键菜单管理已经…

作者头像 李华
网站建设 2026/5/2 5:13:28

Mac软件管理的终极解决方案:免费工具Applite完全指南

Mac软件管理的终极解决方案&#xff1a;免费工具Applite完全指南 【免费下载链接】Applite User-friendly GUI macOS application for Homebrew Casks 项目地址: https://gitcode.com/gh_mirrors/ap/Applite 还在为Mac上的软件安装和更新而烦恼吗&#xff1f;传统的命令…

作者头像 李华