EventPool 32-bit 带标签 CAS 池:RT-Thread 单核 + Linux 双平台无锁

EventPool 32-bit 带标签 CAS 池:RT-Thread 单核 + Linux 双平台无锁

项目链接:https://github.com/DeguiLiu/coact

嵌入式事件池同时要满足三个约束:ISR 安全(定时器随时可能调 alloc())、无 libatomic(Cortex-M3/M4 的 GCC 默认不提供 64-bit 原子操作,std::atomic<uint64_t> 会回退到锁实现)、热路径零堆。三个约束指向同一个方案------32-bit tagged CAS + CriticalSection 注入。
#mermaid-svg-2OyLz72pUz0YGNEV{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-2OyLz72pUz0YGNEV .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-2OyLz72pUz0YGNEV .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-2OyLz72pUz0YGNEV .error-icon{fill:#552222;}#mermaid-svg-2OyLz72pUz0YGNEV .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-2OyLz72pUz0YGNEV .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-2OyLz72pUz0YGNEV .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-2OyLz72pUz0YGNEV .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-2OyLz72pUz0YGNEV .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-2OyLz72pUz0YGNEV .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-2OyLz72pUz0YGNEV .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-2OyLz72pUz0YGNEV .marker{fill:#333333;stroke:#333333;}#mermaid-svg-2OyLz72pUz0YGNEV .marker.cross{stroke:#333333;}#mermaid-svg-2OyLz72pUz0YGNEV svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-2OyLz72pUz0YGNEV p{margin:0;}#mermaid-svg-2OyLz72pUz0YGNEV .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-2OyLz72pUz0YGNEV .cluster-label text{fill:#333;}#mermaid-svg-2OyLz72pUz0YGNEV .cluster-label span{color:#333;}#mermaid-svg-2OyLz72pUz0YGNEV .cluster-label span p{background-color:transparent;}#mermaid-svg-2OyLz72pUz0YGNEV .label text,#mermaid-svg-2OyLz72pUz0YGNEV span{fill:#333;color:#333;}#mermaid-svg-2OyLz72pUz0YGNEV .node rect,#mermaid-svg-2OyLz72pUz0YGNEV .node circle,#mermaid-svg-2OyLz72pUz0YGNEV .node ellipse,#mermaid-svg-2OyLz72pUz0YGNEV .node polygon,#mermaid-svg-2OyLz72pUz0YGNEV .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-2OyLz72pUz0YGNEV .rough-node .label text,#mermaid-svg-2OyLz72pUz0YGNEV .node .label text,#mermaid-svg-2OyLz72pUz0YGNEV .image-shape .label,#mermaid-svg-2OyLz72pUz0YGNEV .icon-shape .label{text-anchor:middle;}#mermaid-svg-2OyLz72pUz0YGNEV .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-2OyLz72pUz0YGNEV .rough-node .label,#mermaid-svg-2OyLz72pUz0YGNEV .node .label,#mermaid-svg-2OyLz72pUz0YGNEV .image-shape .label,#mermaid-svg-2OyLz72pUz0YGNEV .icon-shape .label{text-align:center;}#mermaid-svg-2OyLz72pUz0YGNEV .node.clickable{cursor:pointer;}#mermaid-svg-2OyLz72pUz0YGNEV .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-2OyLz72pUz0YGNEV .arrowheadPath{fill:#333333;}#mermaid-svg-2OyLz72pUz0YGNEV .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-2OyLz72pUz0YGNEV .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-2OyLz72pUz0YGNEV .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2OyLz72pUz0YGNEV .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-2OyLz72pUz0YGNEV .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2OyLz72pUz0YGNEV .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-2OyLz72pUz0YGNEV .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-2OyLz72pUz0YGNEV .cluster text{fill:#333;}#mermaid-svg-2OyLz72pUz0YGNEV .cluster span{color:#333;}#mermaid-svg-2OyLz72pUz0YGNEV div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-2OyLz72pUz0YGNEV .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-2OyLz72pUz0YGNEV rect.text{fill:none;stroke-width:0;}#mermaid-svg-2OyLz72pUz0YGNEV .icon-shape,#mermaid-svg-2OyLz72pUz0YGNEV .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2OyLz72pUz0YGNEV .icon-shape p,#mermaid-svg-2OyLz72pUz0YGNEV .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-2OyLz72pUz0YGNEV .icon-shape .label rect,#mermaid-svg-2OyLz72pUz0YGNEV .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2OyLz72pUz0YGNEV .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-2OyLz72pUz0YGNEV .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-2OyLz72pUz0YGNEV :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 约束三角
ISR 安全

不可阻塞
无 libatomic

Cortex-M3/M4
热路径零堆
32-bit tagged CAS + CriticalSection

为什么 32-bit:容量、索引、tag 三者的关系

池的 free-list 是索引式 Treiber 栈,每块首 4 字节存 next 索引。free_head 是一个 32-bit std::atomic<uint32_t>,高 16 位 tag、低 16 位 index:

cpp 复制代码
inline uint32_t pool_pack_head(uint32_t index, uint32_t tag) noexcept {
    return ((tag & 0xFFFFU) << 16) | (index & 0xFFFFU);
}

32-bit 的取舍由三处静态约束绑定(EventPool 模板):

cpp 复制代码
static_assert(Capacity > 0U, "...");
static_assert(Capacity < 0xFFFFU,
              "EventPool capacity must fit the 16-bit free-list index");
static_assert(pool_block_align(BlockSize) <= 0xFFFFU, "...");
  • 容量上限是 16-bit 引出的 :低 16 位索引最多表达 65535 块(pool_index_invalid = 0xFFFF 又占掉一个 sentinel,实际可用 65534)。嵌入式池子几十到几百块,够用。
  • tag 是 16-bit 的:每次 CAS 成功 tag+1,回绕需 65536 次成功操作。高竞争下若单核池每秒上万次 alloc/reclaim,回绕周期仍在秒级------但单核有 CriticalSection 串行化,tag 回绕时索引相同但不构成 ABA 危(详见下节)。真正的 ABA 防护在 SMP host 上由"tag 回绕周期远大于竞争窗口"兜底。
  • 关键收益 :32-bit CAS 在 x86 和 ARM Cortex-M 的 LDREX/STREX 上都是原生指令,不触发 libatomic 的 64-bit 锁回退。这是选 32-bit 而非 newosp DataDispatcher 的 64-bit tagged 方案的核心动机。

CriticalSection 注入:单核 ISR 安全,而不仅是原子

单核 MCU 上 CAS 只防"多核并发",不防"ISR 抢占"------ISR 可以在 LDR 和 STREX 之间插入,打断唯一核上的操作。所以在 head RMW 前后包一层中断屏蔽:

cpp 复制代码
Event* alloc(uint16_t signal) noexcept {
    const CriticalSection::Token tok = cs_.save(cs_.ctx);  // irq mask (RT-Thread)
    uint32_t head = record_.free_head.load(std::memory_order_relaxed);
    unsigned spin = 0U;
    for (;;) {
        const uint32_t idx = pool_head_index(head);
        if (pool_index_invalid == idx) break;
        const uint32_t nxt = load_next(idx);
        const uint32_t new_head = pool_pack_head(nxt, pool_head_tag(head) + 1U);
        if (record_.free_head.compare_exchange_weak(
                head, new_head,
                std::memory_order_relaxed, std::memory_order_relaxed)) {
            claimed = idx; break;
        }
        pool_backoff(head, record_.free_head, spin);
    }
    cs_.restore(cs_.ctx, tok);
    return construct_from(claimed);
}

CriticalSection 由函数指针注入(pal.hpp),不写死在池里:

  • RT-Threadsave/restore 映射到 rt_hw_interrupt_disable/enable,O(1),一次 head RMW 就是"关中断 + CAS + 开中断",ISR 在整个操作期间被屏蔽,天然串行化。
  • POSIX host 单线程测试:注入 no-op,CAS 自行保证并发。

这里内存序用 relaxed 而不是 acquire/release:单核下 irq mask 提供的序列化比任何 barrier 都强,CAS 的 RMW 原子性已够;多核下 free_head 的 RMW 本身成序,tag 随 CAS 单调递增又排除了 ABA。relaxed 省掉不必要的屏障,这也是 pal.hpp 注释里"per contract 4.3"的纪律------CriticalSection 内的 CAS 一律 relaxed,临界区之外才需要 acquire/release 给数据可见性。

有界后退:避免 CAS 风暴

Treiber 栈的经典病:高竞争下每个失败者立即重试 CAS,触发 exclusive cache-line 风暴。pool_backoff 在每次失败后增加 relaxed load 次数(指数退避,上限 8 次),让 winner 的写落地、cache line 重新静默:

cpp 复制代码
inline void pool_backoff(uint32_t& head,
                         const std::atomic<uint32_t>& free_head,
                         unsigned& spin) noexcept
{
    ++spin;
    if (spin > 3U) { spin = 3U; }   // 上限 2^3=8 次 relaxed load
    const unsigned width = 1U << spin;
    for (unsigned i = 0U; i < width; ++i) {
        head = free_head.load(std::memory_order_relaxed);
    }
}

要点:退避内用 relaxed load(共享读) 而非 CAS(独占读),这样退避的核心不持续持有 cache-line exclusive 锁,winner 的写能落地,次轮 CAS 有更大成功率。head 在退避中被重新 load 刷新,循环继续用最新 head 做 CAS。

为什么"单核 + tag 回绕"仍安全

有人会问:16-bit tag 回绕后,pool_pack_head(same_index, tag) 可能拿到与回绕前相同的值,CAS 会不会误判成功、把 ABA 重新引入?单核上不会,因为 CriticalSection 让 alloc/reclaim 不可能并发------池的 head 操作在一个 irq mask 临界区内串行完成,不存在两个线程各自持着一个旧快照去 CAS 的时序。ABA 需要"读头 → 别的线程改头 → 自己用旧头 CAS",单核屏蔽中断后这个窗口不存在。多核上 tag 回绕周期(65536 次成功操作)远长于一个头竞争窗口(纳秒~微秒级),实际不可达。

性能

RT-Thread 单核上 32-bit CAS 是原生指令,irq mask 是 O(1),Release 构建不引入框架锁。实测(RelWithDebInfo, 100 核竞争):

  • direct 模式吞吐 +13%(15.4M → 17.4M ev/s);
  • staged 模式聚合吞吐 -12~15%(单共享 free_head 被多生产者 alloc 与单消费者 reclaim 同时锤击,cache-line ping-pong。这启发了后续的批量回收 ReclaimBatcher,见另一篇)。

单核 RT-Thread 上池锁本非热点,此改动偏向中性------它的价值在"无锁 + 无 libatomic + ISR 安全"三者同时成立,而不是吞吐数字。


相关推荐
艾莉丝努力练剑1 小时前
【Qt:问题解决】Qt程序入口点错误修复:MinGW ABI兼容问题
java·服务器·开发语言·qt·文件系统·动静态库
雾时之林2 小时前
Linux------磁盘管理
linux·运维
数智工坊10 小时前
Zotero自建Ubuntu WebDAV附件同步方案(完美解决存储空间不足)
linux·运维·ubuntu
江畔柳前堤10 小时前
HBM:大语言模型时代的「算力血液」——从内存墙到带宽革命的深度拆解
服务器·人工智能·windows·目标检测·语言模型·自然语言处理·软件工程
ltl11 小时前
HTTP/3 实战:从 QUIC 到 H3 的完整请求链路
linux
独隅11 小时前
GitHub Actions 自动化运维实战:CI/CD 流水线一体化效果展示
运维·自动化·github
ltl11 小时前
POSIX 文件锁:flock、fcntl 与 NFS 上的工程陷阱
linux
ltl11 小时前
CUDA 生态:cuBLAS、cuDNN、NCCL、Triton、CUTLASS
linux
饺子大魔王的男人11 小时前
CentOS部署Cockpit:图形化查看系统、日志与服务状态
linux·运维·centos