一键导入
concurrency-audit
新增或修改原子操作、异步定时器、无锁通道、跨线程状态同步、CAS 竞争相关代码时触发。
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
新增或修改原子操作、异步定时器、无锁通道、跨线程状态同步、CAS 竞争相关代码时触发。
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
写入新模块文档、重构模块、或用户要求分析模块时触发。
Bug 修复或对接问题排查确认有效后,将经验记录到知识库。
修改 PMR 分配器、内存池配置、热路径容器、对象生命周期管理代码后触发。
编写或修改性能基准测试、分析 benchmark 结果、优化热路径性能时触发。
新增或修改 enable_shared_from_this 类、co_spawn 调用、shared_ptr 捕获的 lambda、co_await 后的成员访问时触发。
修改协程(co_await/co_return/co_spawn)、异步 I/O、定时器、多线程相关 C++ 代码时触发。
| name | concurrency-audit |
| description | 新增或修改原子操作、异步定时器、无锁通道、跨线程状态同步、CAS 竞争相关代码时触发。 |
在每线程单执行器的协程架构中,大部分协程天然串行,不需要传统同步原语。但跨线程状态同步、资源限额、异步定时器、协程间信号等场景仍然依赖原子操作和异步通知。这些原语虽然"允许使用",但用错了比不用更危险——错误通常是偶发的、难以复现的、且在特定负载模式下才暴露。
本 skill 与 coroutine-audit 互补:coroutine-audit 管"禁止什么"(禁止阻塞、禁止互斥锁),本 skill 管"允许但要用对"(原子序要用对、定时器要管理好、通道语义要理解)。
std::atomic 成员或修改原子操作的内存序co_spawn(尤其是涉及共享状态的协程)很多开发者习惯所有原子操作都用 memory_order_seq_cst(最强序),觉得"安全"。但在协程架构中:
io_context 上的两个协程永远不会并发执行。对同一执行器上的状态同步,relaxed 足够acq_rel 保证因果性relaxed 是正确选择。过强的序不仅无意义,还会引入不必要的 CPU 屏障开销"检查然后操作"(Time Of Check To Time Of Use)问题存在于所有 CAS 场景中:
在资源限额场景中,TOCTOU 导致的"偶尔多接受一个连接"通常可接受。但如果限额是安全硬约束(如合规要求),则需要更强的机制。
| 检查项 | 说明 |
|---|---|
| 同步模式用 acq_rel | 跨线程的状态同步(如"停止"标志位的发布-订阅、任务队列的入队通知)需要 release(写端)+ acquire(读端)配对。release 保证写端之前的所有写操作对读端可见,acquire 保证读端之后的所有读操作看到最新值 |
| 统计用 relaxed | 计数器、仪表盘等纯统计场景只需要原子性(不撕裂),不需要因果序。relaxed 保证读写的原子性但不保证顺序——对"最终一致即可"的统计完全正确 |
| 标志位用 release/acquire | 单向通知(如"已关闭"、"已初始化")用 release 写入、acquire 读取。比 acq_rel 更精确:写端只保证"之前的写可见",不需要同步读端的状态 |
| 避免 seq_cst 滥用 | seq_cst 是最强序,隐含全局全序。除非需要"所有线程看到相同的操作顺序"(如全局初始化屏障),否则不应使用。过度使用 seq_cst 引入不必要的内存屏障,在 ARM 等弱序架构上性能影响显著 |
| 类型选择正确 | atomic<bool> 和 atomic<uint32_t> 是否选对了类型?用 uint32_t 存 0/1 是反模式,应直接用 atomic<bool>。用 bool 存多状态(0/1/2)是溢出风险 |
| 检查项 | 说明 |
|---|---|
| cancel-then-destroy 顺序 | 定时器销毁前是否先取消?未取消的定时器在销毁后仍可能触发回调,导致访问已销毁的对象。正确模式:先 cancel() 投递取消请求,再销毁定时器对象。在单线程执行器上,cancel 投递的 handler 会在当前协程挂起后执行——如果定时器对象在此之前被销毁,需要确保 handler 中的 self 保活 |
| 共享定时器的双投递语义 | 两个协程共享一个定时器时,第二个 expires_after() 会取消第一个的等待。功能上正确(隐式取消),但依赖执行器的隐式取消保证,重构时容易引入 bug。如果两个协程需要独立的超时,应使用独立的定时器 |
| 取消路径的异常安全 | 定时器取消后 async_wait 返回 operation_aborted 错误码。调用方是否正确处理了这个错误码?如果用 redirect_error 吞掉了错误码,需要确认取消是预期行为而非意外 |
| 定时器回调中的对象存活 | 定时器的 async_wait 回调中引用的对象是否保证在回调触发时仍然存活?如果对象可能先于定时器销毁,需要用 self 捕获保活或在销毁前显式取消定时器 |
| 检查项 | 说明 |
|---|---|
| CAS 失败的降级路径 | CAS 失败后是否有合理的降级?重试(CAS 循环)适用于低竞争场景;异步等待(co_await 信号)适用于高竞争场景。无限重试的 CAS 循环在竞争激烈时退化为忙等待——参见 coroutine-audit 的禁止清单 |
| 限额检查的近似性 | 连接数限额的 CAS 检查在高并发下可能偶尔超出限额(N 个线程同时读到相同计数器值,全部通过检查)。如果严格限额是硬约束,需要更强的同步(如全局计数器 + 独占检查)。如果近似限额可接受,在审计报告中显式标注"近似限额"并说明超出的最大值 |
| ABA 问题的可能性 | 值从 A → B → A 时 CAS 成功但语义错误。典型场景:指针从 P1 → P2 → P1(P1 被回收后重新分配)。对于简单的计数器(只增减不回环),ABA 不是问题。对于指针/索引的 CAS,需要额外机制(如版本号、epoch) |
| exchange 的幂等性 | 用 exchange 设置"已执行一次"标志时,第二次调用是否安全?exchange 返回旧值但不阻止重复调用。如果重复调用有副作用(如重复通知、重复释放),需要在调用前先检查返回值 |
| 检查项 | 说明 |
|---|---|
| 通道容量与背压 | 无界通道不会阻塞发送者,但消费者跟不上时内存无限增长。有界通道在满时阻塞发送者,提供背压但可能导致死锁。选择容量时需要评估消费者的处理速度和峰值缓冲需求 |
| try_send 的丢弃风险 | 非阻塞的 try_send 在通道满时静默丢弃消息。丢弃是否可接受?如果消息是错误通知,丢弃可能导致下游挂起。如果下游同时有其他感知机制(如 transport 关闭),丢弃可接受 |
| 关闭后的消费终止 | 通道关闭后,正在消费的循环是否能正确终止?标准模式是"关闭返回空/错误"。如果关闭后仍尝试 send,需要确认行为是静默失败还是触发断言 |
| 单写者保证 | 通道是否为单写者?如果不是,多个写者的并发发送是否线程安全?标准无锁通道通常支持多写者,但自定义通道可能不支持 |
| 检查项 | 说明 |
|---|---|
| 同执行器无竞争 | 同一 io_context 上的协程天然串行,不构成数据竞争。同执行器上的共享状态不需要原子操作——普通成员变量足够。但需要文档化"此状态仅在 X 执行器上访问"的约定,防止未来维护者误跨线程使用 |
| strand 的必要性 | 如果多个执行器需要访问同一状态,是否使用了 strand(序列化包装器)?strand 保证所有通过它投递的操作串行执行,是"多执行器单状态"的标准解决方案。但 strand 引入额外的调度开销,应仅在必要时使用 |
| 跨线程派发的正确性 | 使用 post(target_executor, handler) 跨线程派发任务时,handler 中访问的对象是否属于目标执行器?跨线程派发后,原线程上的对象状态可能已经变化 |
atomic、定时器、通道、co_spawn 调用// ❌ 所有原子操作都用 seq_cst — 不必要的性能开销
counter_.fetch_add(1, std::memory_order_seq_cst);
flag_.store(true, std::memory_order_seq_cst);
// ✅ 统计用 relaxed,标志用 release
counter_.fetch_add(1, std::memory_order_relaxed);
flag_.store(true, std::memory_order_release);
// ❌ 定时器未取消直接销毁 — 回调可能访问已销毁对象
void on_close()
{
timer_.reset(); // 销毁定时器,但 async_wait 的回调可能已在队列中
}
// ✅ 先取消再销毁
void on_close()
{
timer_.cancel(); // 投递取消请求
// 单线程执行器上,cancel 后的回调在当前协程挂起后执行
timer_.reset();
}
// ❌ 无限 CAS 循环 — 高竞争下退化为忙等待
while (true)
{
auto old = counter_.load();
if (counter_.compare_exchange_weak(old, old - 1))
{
break;
}
// 无降级,持续消耗 CPU
}
// ✅ CAS 快路径 + 异步等待慢路径
while (!acquired && active())
{
auto old = window_.load(std::memory_order_acquire);
if (old >= needed && window_.compare_exchange_weak(old, old - needed))
{
acquired = true;
break;
}
// 降级:异步等待信号,不忙等
co_await signal_->async_wait();
}
// ❌ 错误通知用 try_send — 通道满时丢弃,下游挂起
void on_error(error_code ec)
{
channel_.try_send(ec); // 满时静默丢弃
}
// ✅ 确保错误通知不被丢弃,或使用兜底机制
void on_error(error_code ec)
{
channel_.try_send(ec); // 尝试发送
transport_->close(); // 兜底:关闭 transport,下游感知
}
// ❌ 多个连接并发修改共享的上下文对象
void setup(connection& conn)
{
conn.shared_context()->set_callback(my_callback);
// 另一个线程的连接可能同时修改同一个 shared_context
}
// ✅ 在连接级别设置回调,而非修改共享对象
void setup(connection& conn)
{
conn.set_callback(my_callback); // 修改连接自己的状态
}
coroutine-audit 覆盖了禁止阻塞调用、禁止互斥锁、协程纯度维度co-lifecycle-audit 覆盖了 shared_ptr 循环引用、co_await 后引用失效维度error-chain-audit 覆盖了异步任务的错误处理和异常安全维度crypto-audit 覆盖了常量时间比较、时序侧信道防御维度probe-audit 覆盖了响应时间分布一致性维度