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 の enum 値を渡します。デフォルトは SeqCst なので、オーダリングを気にしない呼び出し側は最も強い保証を得られます。

Shared\Counter ではなく Atomic を使うべき場面:

  • ステートマシンidle → busy → done のような遷移には compareAndSet を使います。
  • バージョンスタンプ / 世代カウンター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(ロード) — Release ストアと対になります。この操作より後の読み取りは、リリース側が完了していた書き込みを観測します。
  • Release(ストア) — Acquire ロードと対になります。この操作より前の書き込みは、アクワイア側に見えるようになります。
  • AcqRel(read-modify-write) — 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 操作が、その操作にとって無効なメモリオーダリングを受け取った場合。

可観測性

全体像については共有状態の可観測性を参照してください。クイックリファレンス:

  • 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 でラップするか、2 つの Counter(分子 / 分母)を組み合わせてください。
  • ホスト間の協調。 Atomic はプロセス内でのみ有効です。マルチホストの状態には Redis、データベース、またはメトリクスパイプラインを使ってください。
  • 永続性。 Atomic の状態はサーバー停止時に消失します。値が再起動をまたいで残る必要がある場合は、スナップショットを別の場所に永続化してください。

関連

  • 共有状態 — 概要と移行パターン。
  • Shared\Counter — 値がドメインのアキュムレーターである場合。
  • Shared\Mutex — 状態が 1 つの int64 を超えて広がる場合。
  • Shared\Flag — 値が単なるオン/オフである場合。