Shared\Atomic
OxPHP\Shared\Atomic は、プロセス全体で共有されるアトミックな 64 ビット符号付き整数で、プリミティブ操作の全面を備えています。すなわち load、store、swap、compareAndSet に加えて fetchAdd/Sub/And/Or/Xor です。すべての操作はロックフリーで、メモリオーダリングは明示的に指定でき、デフォルトは SeqCst です。
概要
- アトミックな int64 プリミティブ。 範囲は
−9_223_372_036_854_775_808 … 9_223_372_036_854_775_807です。オーバーフローはラップします。 - ロックフリー。 各操作は単一の CPU アトミック命令(
load、store、xchg、cmpxchg、xaddなど)にコンパイルされます。 - メモリオーダリングは自分で選べます。
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 リファレンス
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 |
Relaxed、Acquire、SeqCst |
store |
Relaxed、Release、SeqCst |
swap、fetchAdd、fetchSub、fetchAnd、fetchOr、fetchXor |
任意 |
compareAndSet の success |
任意 |
compareAndSet の failure |
Relaxed、Acquire、SeqCst |
デフォルトはどこでも Ordering::SeqCst なので、オーダリングを意識しない呼び出し側でも安全な挙動が得られます。無効な組み合わせは、FFI 呼び出しの前に OxPHP\Shared\InvalidOrderingException をスローします。
C++/Rust のメモリモデルの詳細については、Rust std::sync::atomic::Ordering のドキュメントを参照してください。
例
compareAndSet によるステートマシン
<?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
$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
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
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 は新しい値ではなく、直前の値を返します。これは、新しい合計を返す Counter::add とは意図的に対照的です。抽象化が異なれば、戻り値の慣習も異なります。意図するセマンティクスに合ったクラスを選んでください。
i64::MIN.fetchSub(1) は i64::MAX を返します。例外はスローされません。
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_total、oxphp_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 — 値が単なるオン/オフである場合。