Shared\Map
OxPHP\Shared\Map は共有レジストリ内に存在し、プロセス内のすべての PHP ワーカーから見える並行マップです。2 つのワーカー、あるいはリクエストハンドラーとバックグラウンドタスクが、リクエストのライフサイクルをまたいで存続する可変状態を共有する必要があるときに、まず選ぶべきプリミティブです。
概要
int|string → mixed。 キーは PHP の整数または文字列で、区別されて保持されます(123と"123"は別のキーであり、PHP の配列のようなキーの型強制は行われません)。文字列キーはバイナリセーフで、不透明なバイト列として(PHP の配列 / Go / Redis のように)保存されるため、非 UTF-8 のキー(NUL の埋め込みを含む)も忠実にラウンドトリップします。値には任意のスカラー、スカラー/配列からなる配列、あるいは別のShareableインスタンスを指定できます。nullは不在を意味し、格納された値として扱われることは決してありません。null値を書き込むとTypeExceptionがスローされます。nullが返された場合は常に「そのようなキーは存在しない」ことを意味します。これにより、get()が null を返す古典的な曖昧さを根本から取り除きます(java.util.concurrent.ConcurrentHashMapや Go のsync.Mapと同じ選択です)。- 単一の線形化可能な条件付きプリミティブ。
compareAndSetはnullという不在センチネルを介してアトミックな挿入 / 置換 / 削除をカバーします。あらゆる read-modify-write はこの上に構築します。 - 並行。 異なるワーカーからの書き込みに外部ロックは不要です。キーごとの操作はシャードレベルでアトミックです。
- 循環に対して安全。 この Map へと戻って到達するような
Shareableを格納しようとすると、いかなる変更が行われる前にCycleExceptionで拒否されます。拒否された経路でリークは発生しません。 - おおよそのソフト上限で制限されます。
maxEntriesは OOM 対策の上限であり、正確なカウントではありません。
API リファレンス
namespace OxPHP\Shared;
final class Map implements Shareable
{
public function __construct(?int $maxEntries = null); // null = unbounded; <= 0 throws TypeException
// reads
public function get(int|string $key): mixed; // null ⟺ absent
public function getMany(iterable $keys): \Iterator; // lazy; skips absent keys
public function count(): int; // striped, weakly consistent
public function maxEntries(): ?int;
// writes
public function set(int|string $key, mixed $value): void;
public function setIfAbsent(int|string $key, mixed $value): mixed; // prev; null ⟺ inserted
public function setMany(iterable $entries): int;
public function remove(int|string $key): bool; // existed?
public function removeMany(iterable $keys): int;
public function clear(): int; // entries removed
// value-returning
public function swap(int|string $key, mixed $value): mixed; // prev; null ⟺ was absent
public function pop(int|string $key): mixed; // prev; null ⟺ was absent
// conditional (single linearisable primitive)
public function compareAndSet(int|string $key, mixed $expected, mixed $new): bool;
// iteration
public function forEach(callable $fn): void; // weakly consistent; callback runs lock-free
public function id(): int;
}| メソッド | ユースケース |
|---|---|
__construct |
オプションの maxEntries 上限付きで生成します(null = 無制限、<= 0 はスロー)。 |
get |
キーで取得します。null ⟺ 不在。 |
getMany |
既知のキーについて key => value を遅延ストリームします。不在のキーはスキップされます(後述)。 |
count |
おおよそのエントリ数(並行書き込み下では弱一貫性)。 |
maxEntries |
設定された上限を報告します(無制限の場合は null)。 |
set |
挿入または置換します。以前の値はマテリアライズされません。 |
setIfAbsent |
存在しなければアトミックに挿入します。既存の値を返し、挿入した場合は null を返します。 |
setMany |
任意の iterable から一括挿入します。書き込んだ件数を返します。 |
remove |
キーを削除します。存在していたかどうかを返します(値はマテリアライズされません)。 |
removeMany |
一括削除します。実際に削除された件数を返します。 |
clear |
すべてのエントリを破棄します(ネストされた Shareable の保持を解放します)。削除した件数を返します。 |
swap |
上書きして以前の値を返します(null ⟺ 不在だった)。 |
pop |
削除して以前の値を返します(null ⟺ 不在だった)。 |
compareAndSet |
現在の内容を条件にアトミックな挿入 / 置換 / 削除を行います(後述)。 |
forEach |
弱一貫性のトラバース。コールバックはロックを保持せずに実行されます。 |
id |
数値のレジストリ識別子。ロギングや /__ox_shared/entry?id=… に便利です。 |
has()、update()、getOrSet()、keys()、trySet()、updateMany() は存在せず、このクラスは Countable を実装していません — 旧来の API からの移行を参照してください。
null を不在とするモデル
null はあらゆる場所で不在センチネルとして予約されています。
null値を伴うset/swap/setIfAbsent→TypeException。nullを返すget/swap/pop/setIfAbsent⟺ キーが不在だった。compareAndSetでは、どちらの側のnullも「不在」を意味します(「null を格納する」ではありません)。
「値なし」を記録する必要がある場合は、null を格納するのではなく、キーを削除してください(またはキーの不在を利用してください)。has() が存在せず、has()+get() の並行レースも存在しないため、存在確認は単一の get($k) !== null でアトミックに行えます。
compareAndSet — 条件付きプリミティブ
$map->compareAndSet($key, expected: null, new: $v); // insert iff absent (= setIfAbsent, returns bool)
$map->compareAndSet($key, expected: $a, new: $b); // replace iff current === $a
$map->compareAndSet($key, expected: $a, new: null); // remove iff current === $aスワップが適用された場合にのみ true を返します。等価性は内容による判定です。スカラーは値で、文字列と配列はシリアライズされたバイト列で、ネストされた Shareable 値はレジストリの同一性で比較されます。配列の等価性は、一般的なケース(リスト、すべて int キーまたはすべて string キーの配列)では PHP の === と一致します。int キーと string キーが混在する配列は Map の正規化された格納形式で比較されるため、特定の int/string の並び順は区別されません(Map は読み戻し時にもそうした配列を並べ替えます)。read-modify-write は明示的なリトライループとして構築してください。競合下では複数回実行されるため、クロージャは純粋に保ってください。
do {
$cur = $map->get('counter'); // null if absent
$next = ($cur ?? 0) + 1;
} while (!$map->compareAndSet('counter', $cur, $next));ここに ABA 問題はありません。ストアはコンテンツアドレス方式であり(値ストアにおいて、内容が等しい値は同じ値です)、ネストされた Shareable の同一性は単調増加し再利用されないレジストリ ID を使用します。スタンピードに対して安全な遅延初期化には Shared\Once を、プールされたリソースには Shared\Pool を使用してください。
メモリモデル — コピーが発生する場所
値は zval としてではなく、シリアライズされた表現として格納されます。そのため「ゼロコピー」は値には当てはまりません。
| 操作 | 共有ヒープへのシリアライズ | 以前の値を zval へマテリアライズ |
|---|---|---|
set / setMany |
はい | いいえ |
remove / removeMany |
— | いいえ |
setIfAbsent |
はい | 以前の値が存在する場合のみ |
swap / pop |
はい / — | はい |
get / getMany |
(キーのみ) | はい |
compareAndSet |
はい($new) |
いいえ |
書き込みパスでのシリアライズは、共有メモリに入るあらゆる値にとって避けられません。新しい zval への読み戻しのコストは、以前の値/参照した値を返すメソッドだけが支払います。したがって set/remove は「戻り値のマテリアライズがない」のであって、「無料」ではありません。例外はネストされた Shareable 値です。これは(id と参照カウントのインクリメントとして)参照で格納され、ディープコピーされません。
並行処理
count()は弱一貫性です。 エントリ数はシャードごとにストライプ化され、読み取り時に合計されます。結果はマップが静止しているときは正確で、並行書き込み下では近い近似値になります(ConcurrentHashMap::sizeと同じ契約です)。ストライピングにより、書き込みが単一のホットなカウンターに集中しないようにします。maxEntriesはソフト上限です。 これはストライプ化された合計に対してチェックされるため、並行挿入下では、CapacityExceptionで新しいキーを拒否する前に、マップがシャード数の分だけ超過することがあります。正確なカウントではなく、OOM 対策の予算として扱ってください。既存のキーの上書きは、上限に達していても常に成功します。エビクションはありません — LRU/TTL エビクションを備えたキャッシュは別のプリミティブです。forEachはロックを保持せずにコールバックを実行します。 一度に 1 つのシャードのキーをスナップショットし、シャードを解放してから、各値を再取得して$fn(key, value)を呼び出します。スナップショットと呼び出しの間に削除されたキーはスキップされます。シャードのスナップショット後に追加されたキーは見落とされる可能性があります。値はスナップショット時点よりも新しい場合があります。早期に停止するにはコールバックからfalseを返します。キーのみがスナップショットされるため、遅いコールバックが削除された値を固定し続けることはありません。
例
共有設定キャッシュ
<?php
$config = new OxPHP\Shared\Map(maxEntries: 1024);
// Warm once at app bootstrap.
$config->setMany([
'rate_limit.default_rpm' => 600,
'feature.new_checkout' => true,
'timeout.downstream_ms' => 250,
]);
// Any request handler reads without contention; null ⟺ not configured.
$rpm = $config->get('rate_limit.default_rpm') ?? 60;テナントごとのレート制限
<?php
$buckets = new OxPHP\Shared\Map(maxEntries: 50_000);
$key = "tenant:{$tenantId}";
$prev = $buckets->setIfAbsent($key, ['tokens' => 100, 'refill_at' => time() + 60]);
// $prev === null ⟺ we created the bucket; otherwise it holds the existing one.
$state = $buckets->get($key);
if ($state['tokens'] === 0) {
throw new RateLimitException();
}ワーカー間でのカウンターの調整
<?php
$counters = new OxPHP\Shared\Map();
$counters->set('requests_handled', new OxPHP\Shared\Counter());
// Any worker increments via the stored Shareable (stored by reference).
$counters->get('requests_handled')->add();大きなマップのイテレーション
<?php
$sessions->forEach(function (int|string $key, mixed $value): bool|null {
if ($value['expires_at'] < time()) {
// safe: forEach holds no lock during the callback
return null; // keep going
}
return null;
});
// Or read a known subset lazily, stopping early:
foreach ($cache->getMany($hotKeys) as $key => $value) {
if (enoughCollected()) break; // remaining keys are never materialised
handle($key, $value);
}セマンティクスと注意点
配列は読み取り時にコピーされる
<?php
$m = new OxPHP\Shared\Map();
$m->set('cfg', ['timeout' => 5, 'retries' => 3]);
$cfg = $m->get('cfg');
$cfg['timeout'] = 10; // mutates the returned copy only
// $m->get('cfg')['timeout'] is still 5配列値をアトミックに更新するには、それを読み取り、コピーを変更し、compareAndSet でコミットします(競合時はリトライします)。あるいは、独立して変化するフィールドをネストされた Shared\Counter / Shared\Map として格納します。
ネストされた Shareable の retain は自動
<?php
$map = new OxPHP\Shared\Map();
$counter = new OxPHP\Shared\Counter(10);
$map->set('c', $counter);
$retrieved = $map->get('c'); // same Shareable identity
$retrieved->add(); // mutation visible via $counter too
echo $counter->get(); // 11
$counter2 = $map->pop('c'); // Map releases its hold, returns the value
$counter2->add(); // still alive via the returned wrapper循環検出は変更前に拒否する
<?php
$a = new OxPHP\Shared\Map();
$b = new OxPHP\Shared\Map();
$a->set('b', $b); // fine
try {
$b->set('a', $a); // closes the loop
} catch (OxPHP\Shared\CycleException $e) {
// message: "cycle would form: #… → #… (inserting into #…)"
}
$b->get('a'); // null — $b untouched, no leaked retains配列内のネストされた参照もチェックされます。ウォーカーは SHARED_CYCLE_DETECT_DEPTH(デフォルト 16)と SHARED_CYCLE_DETECT_EDGES(デフォルト 10 000)で制限されます。非常に大きなグラフでは bounds exceeded を伴う CycleException が発生します。環境変数のつまみを引き上げるか、グラフを分割してください。
値ごとのサイズ上限
シリアライズ後のサイズが SHARED_MAX_VALUE_SIZE(デフォルト 1 MiB)を超える単一の値は、ValueTooLargeException で拒否されます。これは PHP 側の入力によるアロケーション爆弾を防ぎます。すべての書き込みパス(set、setIfAbsent、swap、compareAndSet、setMany)に適用されます。
バッチ操作はキーごとにアトミックであり、バッチ単位ではアトミックではない
setMany、getMany、removeMany は一度に 1 つのキーを処理します。setMany が途中で CapacityException、CycleException、ValueTooLargeException に遭遇した場合、それ以前のキーは格納されたまま残ります。この部分的な成功は意図的なものです。オールオアナッシングのセマンティクスが必要な場合は、マップの周りで Shared\Mutex を使用してください。
旧来の API からの移行
これは互換性シムのない破壊的な再設計です。
| 旧 | 新 |
|---|---|
has($k) |
get($k) !== null(アトミック — has/get レースなし) |
get($k, $default) |
get($k) ?? $default |
trySet($k, $v): bool |
setIfAbsent($k, $v): mixed(以前の値を返す。null ⟺ 挿入された) |
remove($k)(以前の値を返していた) |
remove($k): bool、または値を取得するには pop($k) |
update($k, $fn) |
compareAndSet のリトライループ、または一度きりの初期化には Shared\Once |
getOrSet($k, $fn) |
setIfAbsent、またはケースに応じて Shared\Once / Shared\Pool |
updateMany(...) |
compareAndSet のループ |
keys(): array |
forEach(...)、または getMany($knownKeys) |
count($map)(Countable) |
$map->count() |
null 値の格納 |
キーの不在 / remove を使用 |
例外
失敗しうるすべてのメソッドは OxPHP\Shared\SharedException のサブクラスをスローします。
| 例外 | 発生元 |
|---|---|
CapacityException |
maxEntries を超える新しいキー(set / setIfAbsent / compareAndSet / setMany)。 |
ValueTooLargeException |
値ごとの上限(SHARED_MAX_VALUE_SIZE)を超える値。 |
CycleException |
到達可能性の循環を閉じてしまう書き込み(extends TypeException)。 |
TypeException |
null 値、格納できない値(object/closure/resource)、int/string でないキー、maxEntries <= 0。 |
StaleHandleException |
レジストリエントリがエビクトされたハンドルに対するメソッド呼び出し。 |
可観測性
すべての Map は内部 API を通じて可視化されます。
GET /__ox_shared/summary—Mapを含む、型ごとの集計数。GET /__ox_shared/entries— id / type / refcount / mem_bytes を含むすべてのエントリの一覧。GET /__ox_shared/entry?id=N— Map のインスタンスごとの詳細にはkey_count、max_entries、saturation、sample_keys(プレビュー上限で切り詰められます)が含まれます。GET /__ox_shared/graph?id=N[&depth=D][&edges=E]— 発信 Shareable 参照の BFS 走査。CycleExceptionの後に便利です。
Prometheus は /metrics で Map ごとのゲージを公開します。
| メトリクス | 意味 |
|---|---|
oxphp_shared_map_entries{map_id="…"} |
現在の(おおよその)キー数。 |
oxphp_shared_map_max_entries{map_id="…"} |
設定された上限(無制限の場合は 0)。 |
oxphp_shared_map_saturation{map_id="…"} |
entries / max_entries、無制限の場合は 0。 |
設定
| 環境変数 | デフォルト | 効果 |
|---|---|---|
SHARED_MAX_ENTRIES |
100 000 | すべての Shared エントリを合わせたグローバル上限。 |
SHARED_MAX_BYTES |
1 GiB | すべての Shared エントリにわたる推定メモリのグローバル上限。 |
SHARED_MAX_VALUE_SIZE |
1 MiB | 値ごとのシリアライズ後サイズ上限。これより大きい値は ValueTooLargeException をスローします。 |
SHARED_CYCLE_DETECT_DEPTH |
16 | 循環チェック時の最大 BFS 深度。深い正当なグラフの場合は引き上げます。 |
SHARED_CYCLE_DETECT_EDGES |
10 000 | 循環チェック時に走査する最大エッジ数。密な正当なグラフの場合は引き上げます。 |
SHARED_PREVIEW_ARRAY_LIMIT |
20 | /entry?id=… の sample_keys でサンプリングされるエントリ数。 |
SHARED_INTROSPECTION_ENABLED |
true | /__ox_shared/* API を切り替えます。 |
関連
Shared\Counter— アトミックな整数。キーごとのヒット数のために Map 内に格納します。Shared\Once—setIfAbsentでは高価なファクトリが再実行されてしまう場合の、スタンピードに対して安全な遅延初期化。Shared\Channel— MPMC キュー。キーによる検索ではなく FIFO パイプラインが必要なときに補完的です。Shared\Mutex— 格納された値の周りで厳密な相互排他が必要な場合。