Swoole Atomic 中 cmpset 与 set 的深度区别与正确用法
cmpset 是带条件的原子写入操作,set 则是无条件直接覆盖;cmpset 仅在当前值与预期值完全相等时才会写入新值,并返回操作完成后的当前值,必须通过严格比较返回值来判断是否成功;set 直接覆盖原有数值,无返回值,适用于初始化或重置等无需条件判断的场景。

这两个方法从表面上看都执行“赋值”操作,但本质逻辑截然不同。一个带有条件判断机制,另一个则是直接覆盖。如果选择错误,轻则导致业务逻辑重复执行,重则引发状态数据混乱且难以排查复现。下面我们深入拆解它们的核心差异与应用场景。
cmpset 是条件写入,set 是无条件覆盖
SwooleAtomic::cmpset() 并非简单的赋值函数——它仅在当前值与预期值一致时才执行新值写入;而 set() 无论当前值是什么,都会直接覆盖。这正是两者最根本的区别,也决定了它们各自适用的边界范围。
很多开发者容易犯的一个典型错误,是把 cmpset() 当作“带判断的 set”来使用。例如,想先检查当前值是否为 0 再将其设为 1,却写成:if ($atomic->get() === 0) { $atomic->set(1); }——这中间存在一个明显的竞态窗口,两个进程可能同时通过 get() 判断结果为 0,随后都执行 set(1),最终导致逻辑失控。而 cmpset() 在底层保证了“比较-交换”操作的原子性,多个进程同时调用时,只有一个能成功写入,其余全部失败。
cmpset($cmp_value, $new_value)返回的是操作执行后的当前值(并非布尔值),必须显式进行严格比较:if ($atomic->cmpset(0, 1) === 1) { /* 写入成功 */ }set($value)没有返回值,调用后立即生效,非常适合初始化、重置等无需条件判断的场景- 参数范围一致:两者都要求
$value是 ≤ 4294967295 的非负整数,超出范围会自动截断
cmpset 天然适用于并发竞争场景,set 更适合单次设定
典型的并发场景,例如“仅允许首个 Worker 进程完成配置加载”或“告警开关只触发一次”,必须使用 cmpset()。它底层依赖 CPU 的 cmpxchg 指令,整个“比较-交换”动作不可分割。多个进程同时调用时,只有一个能成功写入,其他全部失败,这才是真正的原子竞争机制。
而 set() 不具备竞争语义,它只负责“写入数值”,不关心“谁应该写入”。适合在服务启动时一次性设定初始状态,或定时任务中强制覆盖某个监控指标(例如每分钟重置计数器)。
- 若需要“首次初始化”,不要使用
set()配合外层 if 判断,而应直接调用一次cmpset(0, 1)并检查返回值 - 若需要“总是设置为某值”,例如清零计数器,
set(0)比反复调用cmpset($cur, 0)更直接,且不存在失败路径 - 混用时需注意:先执行
set()再执行cmpset()没有问题;但cmpset()失败后直接调用set()可能会掩盖本应拒绝的并发冲突
性能与可见性差异不大,但语义误用代价更高
两者底层都基于共享内存加上 CPU 原子指令实现,单次调用开销非常接近。但 cmpset() 的失败重试逻辑(如果存在)会带来实际的性能波动,而 set() 则保持稳定低开销。
真正容易被忽略的是语义层面的陷阱。很多开发者误以为 cmpset() 返回 true/false,于是写出 if ($atomic->cmpset(0, 1)) { ... }——这在 PHP 8+ 中可能因为返回整数被隐式转换为 true 而“看似正常”,但一旦 $new_value 为 0,就永远无法进入分支。因此,务必使用严格比较:=== $new_value,不要依赖真假值转换。
- 始终使用严格比较:
=== $new_value,不要依赖真假值隐式转换 set()不提供任何并发保护能力,它只是原子写入,不解决“谁应该写入”的问题- 如果业务需要“读-改-写”复合操作(例如“加 1 但不超过 100”),
cmpset()无法一步完成,需要借助循环重试或改用Table配合行锁
复杂之处不在于语法本身,而在于你是否清楚自己要解决的是“状态一致性”还是“值覆盖”。用错一个,轻则逻辑重复,重则状态数据错乱,且难以复现。
