Shared\Registry

OxPHP\Shared\RegistryOxPHP\Shared\* 其余部分的按名称索引的搭档。new Shared\Map() 产生的是匿名条目,只能通过句柄传播(use 捕获、异步纤程、嵌套)来共享;而 Registry::map('cache', fn() => new Shared\Map(...)) 会把一个条目绑定到某个字符串键之下。任何工作进程线程、任何请求中的每一个 Registry::map('cache', …) 调用方,都会拿到同一个条目。

它回答的是一个问题:"我该如何在所有工作进程之间、或在传统模式下的所有请求之间,共享同一个 Shared\Map?" 其他 Shared\* 类型仍然是可变状态的恰当单元;Registry 只是你给其中某一个起名字的方式。

心智模型

graph TD
  R["Registry::map('cache', $factory)"]
  W1["工作进程 #1"] --> R
  W2["工作进程 #2"] --> R
  W3["工作进程 #3"] --> R
  R --> S["SharedRegistry(进程全局)<br/>names: { 'cache' → Bound(Arc&lt;E&gt;) }<br/>entries: { id=7: Map { … } }"]
  • 对于尚未绑定的键,第一个调用 Registry::map($key, $factory) 的调用方会运行工厂函数,并把生成的条目按名称固定下来。
  • 之后的每一个调用方(同一线程、其他工作进程、后续请求)都会拿到同一个条目。工厂函数不会重新运行;命中时它会被忽略。
  • 并发的首次访问会在一个按键的门闩上阻塞:恰好一个线程运行工厂函数,其余线程等待并拿到胜出者的条目。这可以避免连接池的重复资源获取。

按名称标识是对按句柄标识的补充。匿名条目(new Shared\*())与具名条目共存于同一个进程全局注册表中。名称索引只是在其之上加了一层字符串查找。

快速上手 —— 在所有工作进程间共享一个计数器

worker.php
<?php // worker.php — entry script in worker mode, executed once per worker thread require __DIR__ . '/vendor/autoload.php'; $requests = OxPHP\Shared\Registry::counter( 'request-counter', fn() => new OxPHP\Shared\Counter(), ); oxphp_worker(function () use ($requests) { $n = $requests->add(); // atomic across ALL workers — one shared int64 header('X-Request-Count: ' . $n); echo "hello\n"; });

可以与捕获句柄的模式对比一下(在 bootstrap 中写 $x = new Shared\Counter())。那种模式会为每个工作进程线程产生一个计数器:每个工作进程运行各自的 bootstrap,拿到各自的匿名条目。聚合出的计数会相差工作进程池大小这么大的倍数。而 Registry::counter('request-counter', …) 会让每个工作进程都汇聚到同一个条目上,因此计数就是真实的总数。

同样的写法在传统模式下(未设置 WORKER_MODE_ENABLED)也适用。第一个访问 'request-counter' 的请求会创建条目;之后的每一个请求(无论在哪个工作进程线程上)都能看到它。这就是同主机上替代 APCu 的方案,用带类型的原语和原子操作取代 apcu_fetch / apcu_store

API 参考

php
namespace OxPHP\Shared; final class Registry { // Typed get-or-create. On hit, the factory is ignored; on miss it // runs at most once across all workers (block-losers) and must // return a fresh instance of the matching type. public static function map(string $key, callable $factory): Map; public static function counter(string $key, callable $factory): Counter; public static function atomic(string $key, callable $factory): Atomic; public static function flag(string $key, callable $factory): Flag; public static function once(string $key, callable $factory): Once; public static function mutex(string $key, callable $factory): Mutex; public static function channel(string $key, callable $factory): Channel; public static function pool(string $key, callable $factory): Pool; // Untyped escape hatch — returns whatever is bound (no type guard). public static function global(string $key, callable $factory): Shareable; // Namespace management — operates on the name index, NOT the objects. public static function remove(string $key): bool; public static function keys(): array; // list<string> // Layer-wide introspection. public static function memoryUsage(): int; // estimated bytes, all Shared\* entries public static function count(): int; // live Shared\* entries (named + anonymous) }
方法 返回 用途
map / counter / atomic / flag / once / mutex / channel / pool 所请求的 Shared\* 类型 主要接口。命中时做类型守卫;工厂返回时做类型校验。
global Shareable 无类型的 get-or-create。只有当你确实事先不知道绑定类型时才用它。
remove bool 丢弃名称绑定与固定引用。不会销毁对象。
keys list<string> 当前已绑定的键(仅 Bound;处于创建中的 Creating 槽位不会列出)。
memoryUsage int 进程范围内的估算字节数 —— 参见 内存与自省
count int 进程范围内的存活条目(具名匿名)。

Registry 是一个静态外观(facade):new Registry() 会抛出 Shared\SharedException

生命周期 —— 默认固定

已绑定的键持有对其条目的引用;除非你显式调用 remove(key) 或进程被拆除,否则条目在整个进程生命周期内都存活。这是有意为之的:在传统模式下,每个请求都会创建各自的 PHP 端句柄,并在请求结束时销毁,此时名称索引的固定引用是条目能在请求之间存活下来的唯一原因。

要让具名条目的内容失效,请就地修改它($cache->clear()$counter->set(0)$bucket->remove($k)),而不是移除名称。修改是按引用共享的:持有同一个键的每个持有者都会立即看到变化。

remove 是命名空间管理,而非对象销毁

remove($key) 会丢弃绑定和固定引用。只要还有任何其他句柄引用它(bootstrap 中捕获的变量、嵌套在另一个 Shared\Map 中的值、进行中的 oxphp_async),条目本身就会继续存活。当最后一个句柄消失时,条目会像往常一样自行注销。

remove 之后,该键就空闲了。下一次 Registry::map($key, …) 会创建一个 id 不同的全新条目。

Warning

指向先前绑定的已捕获句柄会继续操作旧的(现已匿名的)条目;它们不会自动汇聚到新条目上。

php
$cache = Registry::map('cache', fn() => new Shared\Map()); $id_a = $cache->id(); Registry::remove('cache'); $cache->set('x', 1); // still mutates the OLD entry — fine, but it's no longer "cache" $fresh = Registry::map('cache', fn() => new Shared\Map()); $id_b = $fresh->id(); // different id — this is a new entry assert($id_a !== $id_b); assert($cache->get('x') === 1); // OLD entry retained value assert($fresh->get('x') === null); // NEW entry is empty

如果你会轮换键(随来随走的按租户条目、键版本化),请在每次调用时按名称寻址(每个请求都调用 Registry::map($key, …)),而不是在 bootstrap 中只捕获一次句柄。捕获句柄加上键轮换会悄无声息地发散;按名称寻址则始终汇聚到当前的绑定上。

如果丢弃了一个已绑定的键,remove 返回 true;如果该键不存在,则返回 false

错误

异常 何时发生
Shared\TypeException 在绑定到其他类型的键上调用了带类型的方法;工厂返回了错误的 Shared\* 类型或非 Shareable 值。
Shared\CapacityException 创建会超出 SHARED_MAX_ENTRIES / SHARED_MAX_BYTES 上限。
Shared\DeadlockException(重入) 在同一线程上,从某个键自己的工厂内部再对同一个 $key 调用 Registry::map($key, …)
Shared\DeadlockException(跨键环路) 在另一个线程的 Creating 槽位上等待超过 30 s —— 最可能的情况是工厂 A 持有键 K1 并等待 K2,而 K2 的工厂由线程 B 持有,B 又在等待 K1。其消息与重入情形不同。
Shared\SharedException(正在排空) 服务器正在关闭 —— 注册表拒绝新的获取和绑定。这在优雅关闭期间是预期行为;不是代码 bug。
Shared\SharedException(绑定竞争) 在本线程的工厂运行期间,某个对等创建者已经先一步占据了该槽位(工厂的条目没有被固定到该键之下)。重试该调用。
\InvalidArgumentException(SPL) $key 为空。参数校验,与领域类型错误不同。
(工厂抛出的异常) 如果工厂抛出异常,槽位会被中止(Creating → 不存在,等待者被唤醒并重试),原始异常会传播给创建者。

Shared\DeadlockException 继承自 OxPHP\Async\AsyncException,因此 catch (AsyncException) 会把它与 Shared\* 中其他地方的有界等待超时一并捕获。两种不同的 DeadlockException 情形共用同一个类;靠消息来区分它们("reentrant get-or-create""waited too long … cross-key cycle")。

内存与自省

Registry::memoryUsage()Registry::count() 报告的是整个 Shared* 层,而不仅仅是具名条目。通过 new Shared\*() 创建的匿名条目(当前 Shared\* 用法的大头:bootstrap 捕获、MapChannel 内部的在途值、异步纤程捕获)也包含在内。

这是有意为之的。这两个数字是为容量 / OOM 监控而存在的;这类监控必须看到每个工作进程的以及在途的匿名状态,而不只是具名命名空间。因此:

  • 这两个数字都是瞬时的:它们随在途请求和每个工作进程的句柄而起伏。
  • Registry::count() 等于 count(Registry::keys())keys() 仅指具名命名空间。
  • memoryUsage() 是一个静态记账估算值,不是实际的 RSS。它就是 SHARED_MAX_BYTES 所限制的那个数字。要获取真实的堆内存占用,请使用堆性能分析器(heaptrackjemalloc_stats_printmi_stats_print)或容器内存指标。

每个条目的细节(id、类型、引用计数、字节开销)位于内部自省端点/__ox_shared/entries。这里有意不提供逐条目的 PHP API,以避免重复那一层接口。

何时不该使用

  • **跨进程、跨主机。**注册表存在于单个 OxPHP 进程内部。多个 OxPHP 实例之间并不共享它。请使用 Redis / NATS / 你现有的消息代理;参见迁移到外部存储
  • **跨重启的持久性。**注册表会在进程退出时蒸发。请通过同样的外部存储来持久化。
  • **高频变动的临时键。**默认固定的语义意味着你按请求生成的动态键会不断泄漏条目,直到你调用 remove。虽然受 SHARED_MAX_* 上限约束,但仍然是糟糕的做法。对于短命的按请求状态,请使用普通的 PHP 变量。
  • 缓存失效原语。remove($key) 用于退役一个名称,而不是"清空缓存"。请就地让内容失效($map->clear()$map->remove($member_key));名称绑定会保留。

另请参阅