Shared\Atomic

OxPHP\Shared\Atomic 是一个进程级的原子 64 位有符号整数,提供完整的原语接口:loadstoreswapcompareAndSet,以及 fetchAdd/Sub/And/Or/Xor。每个操作都是无锁的,内存序显式可控,默认为 SeqCst

概述

  • 原子 int64 原语。 取值范围 −9_223_372_036_854_775_808 … 9_223_372_036_854_775_807。溢出会回绕。
  • 无锁。 每个操作都编译为单条 CPU 原子指令(loadstorexchgcmpxchgxadd 等)。
  • 内存序由你决定。 当你需要 Relaxed / Acquire / Release / AcqRel / SeqCst 时,传入一个 OxPHP\Shared\Ordering 枚举值。默认值是 SeqCst,因此不关心内存序的调用方会自动获得最强的保证。

何时应该使用 Atomic 而不是 Shared\Counter

  • 状态机 —— 用 compareAndSet 实现 idle → busy → done
  • 版本戳 / 世代计数器 —— fetchAdd(1) 返回之前的版本号;读取方可以据此检测竞争。
  • CAS 循环 —— 用 load 读取,计算新值,反复重试 compareAndSet 直到成功。
  • 位标志掩码 —— 用 fetchOr 置位,用 fetchAnd 清位。

Counter 是做累加(add)的合适工具;Atomic 是处理任意原子状态的合适工具。

API 参考

php
namespace OxPHP\Shared; final class Atomic implements Shareable { public function __construct(int $initial = 0); public function load(Ordering $order = Ordering::SeqCst): int; public function store(int $value, Ordering $order = Ordering::SeqCst): void; public function swap(int $value, Ordering $order = Ordering::SeqCst): int; // returns prev public function compareAndSet( int $expect, int $new, Ordering $success = Ordering::SeqCst, Ordering $failure = Ordering::SeqCst, ): bool; public function fetchAdd(int $delta, Ordering $order = Ordering::SeqCst): int; // returns prev public function fetchSub(int $delta, Ordering $order = Ordering::SeqCst): int; // returns prev public function fetchAnd(int $mask, Ordering $order = Ordering::SeqCst): int; // returns prev public function fetchOr (int $mask, Ordering $order = Ordering::SeqCst): int; // returns prev public function fetchXor(int $mask, Ordering $order = Ordering::SeqCst): int; // returns prev public function id(): int; }
方法 返回值 使用场景
load 当前值 以指定的内存序读取值。
store void 写入新值,丢弃旧值。
swap 之前的值 原子替换;swap(0) 即"快照并清零"模式。
compareAndSet 是否已交换 乐观状态转换和 CAS 循环。
fetchAdd/Sub 之前的值 世代计数器、通过 CAS 实现的有界计数器、增量。
fetchAnd/Or/Xor 之前的值 位标志掩码:置位、清位、翻转。
id 注册表 id 日志、追踪、/__ox_shared/entry?id=… 关联。

内存序

简要说明:

  • Relaxed —— 仅保证原子性,相对于其他内存访问不保证任何顺序。
  • Acquire(用于 load)—— 与 Release 存储配对;该操作之后的读取能观察到 releaser 已经完成的写入。
  • Release(用于 store)—— 与 Acquire 加载配对;该操作之前的写入对 acquirer 可见。
  • AcqRel(用于读-改-写)—— 同时具备 Acquire 加载和 Release 存储两半的语义。
  • SeqCst —— 所有 SeqCst 操作之间存在单一的全局全序。

每个操作只接受对它有意义的内存序:

操作 允许的内存序
load RelaxedAcquireSeqCst
store RelaxedReleaseSeqCst
swapfetchAddfetchSubfetchAndfetchOrfetchXor 任意
compareAndSetsuccess 任意
compareAndSetfailure RelaxedAcquireSeqCst

所有默认值都是 Ordering::SeqCst,因此不去考虑内存序的调用方依然能获得安全的行为。无效的组合会在 FFI 调用之前抛出 OxPHP\Shared\InvalidOrderingException

想深入了解 C++/Rust 内存模型,请参阅 Rust std::sync::atomic::Ordering 文档

示例

通过 compareAndSet 实现状态机

php
<?php use OxPHP\Shared\Atomic; $state = new Atomic(initial: 0); // 0=idle, 1=busy, 2=done if (!$state->compareAndSet(expect: 0, new: 1)) { throw new RuntimeException('another worker is already processing'); } try { doWork(); $state->store(2); } catch (Throwable $e) { $state->store(0); // release back to idle on error throw $e; }

世代计数器 / 版本戳

php
<?php $version = new OxPHP\Shared\Atomic(); // Each writer bumps the version and gets the value it just superseded. $prev = $version->fetchAdd(1); publishUpdate($prev + 1, $payload);

通过 CAS 循环实现乐观更新

php
<?php use OxPHP\Shared\Atomic; use OxPHP\Shared\Ordering; $cell = new Atomic(initial: 100); // Saturate-add: never go above 1000. do { $cur = $cell->load(Ordering::Acquire); $next = min($cur + 7, 1000); if ($cur === $next) { break; // already at cap } } while (!$cell->compareAndSet($cur, $next, Ordering::AcqRel, Ordering::Acquire));

位标志掩码

php
<?php const FLAG_READY = 1 << 0; const FLAG_DRAINING = 1 << 1; const FLAG_FAILED = 1 << 2; $flags = new OxPHP\Shared\Atomic(); $flags->fetchOr(FLAG_READY); // set bit $flags->fetchAnd(~FLAG_DRAINING); // clear bit $snapshot = $flags->load(); if ($snapshot & FLAG_FAILED) { raiseAlert(); }

语义与陷阱

fetchAdd 返回的是之前的值

fetchAdd 返回的是之前的值,而不是新值。这与 Counter::add 形成了刻意的对比——后者返回的是新的总和。不同的抽象,不同的返回约定:选择与你想要的语义相匹配的那个类。

溢出会回绕

i64::MIN.fetchSub(1) 得到的是 i64::MAX。不会抛出任何异常。

默认内存序是 SeqCst

SeqCst 是最安全的选择,也是最慢的。只有在你能清楚说明理由时,才降到 Acquire/Release/Relaxed

一个 Atomic 只持有单个 int64。对于复合状态(多个相互关联的字段),请使用 Shared\Mutex

异常

异常 抛出场景
StaleHandleException 在注册表条目已被逐出的句柄上调用任何方法。
UninitializedException 在尚未完成 __construct 的包装器上调用 id()
InvalidOrderingException 某个操作收到了对它无效的内存序。

可观测性

完整介绍请参阅 Shared 可观测性。快速参考:

  • GET /__ox_shared/entry?id=N 暴露 { value, type: "Atomic" }
  • 注册表级别的计数器(oxphp_shared_operations_totaloxphp_shared_objects_total)通过 type="Atomic" 标签覆盖 Atomic。

何时不该使用

  • 复合状态。 多个必须一起更新的字段 → Shared\Mutex
  • 计数 / 累加。 使用 Shared\Counter —— 它的 add 返回新的总和,更贴合该领域。
  • 浮点数或小数。 不支持;把一个结构体包进 Shared\Mutex,或者配对使用两个 Counter(分子 / 分母)。
  • 跨主机协调。 Atomic 仅在进程内有效。多主机状态请使用 Redis、数据库或指标管道。
  • 持久性。 Atomic 状态在服务器停止时会消失。如果值必须在重启后存活,请把快照持久化到别处。

相关内容