Shared\Counter

OxPHP\Shared\Counter 是一个进程级的原子 64 位有符号整数,专为累加而设计:统计事件、累加增量、滚动窗口总计。每个操作都是无锁的;两个工作进程并发相加时绝不会丢失任何一次计数。

如果需要任意的原子状态且必须同步其他内存(状态机、版本戳、seqlock、位标志掩码),请改用 Shared\Atomic

概述

  • **原子 int64。**取值范围 −9_223_372_036_854_775_808 … 9_223_372_036_854_775_807。溢出会回绕。
  • 无锁。add 会编译为单条 fetch_add
  • **始终是 Relaxed。**操作是原子的(不会丢失计数,不会出现撕裂读),但与其他内存之间不建立 happens-before 关系。Counter 是统计量,而非同步点——如果需要顺序保证,请使用 Shared\Atomic
  • **可共享。**实例可以存放在 Shared\Map / Shared\Channel 中,并通过 use 捕获传递给纤程。

API 参考

php
namespace OxPHP\Shared; final class Counter implements Shareable { public function __construct(int $initial = 0); public function get(): int; // current public function set(int $value): int; // returns previous; set(0) = window reset public function add(int $delta = 1): int; // returns new; add()=+1, add(-1)=decrement public function compareAndSet(int $expect, int $new): bool; public function id(): int; }
方法 返回值 用途
get 当前值 读取而不修改。
set 前一个值 原子交换;set(0) 是窗口结束时的读取并清零。
add 新值 add() 加 1,add(-1) 递减,其他增量则加对应值。
compareAndSet bool 通过 CAS 循环实现有界/饱和计数器(上限、下限)。
id 注册表 id 日志、追踪、/__ox_shared/entry?id=… 关联。

示例

每个工作进程的请求计数器

php
<?php $requests = new OxPHP\Shared\Counter(); oxphp_worker(function () use ($requests) { $count = $requests->add(); // +1, returns the new total header("X-Request-Count: {$count}"); echo "ok"; });

窗口滚动

php
<?php $hits = new OxPHP\Shared\Counter(); // Every N minutes in your cron/worker loop: $prev = $hits->set(0); // atomically reads and zeroes logWindowMetric($prev);

有界计数器(CAS 循环)

php
<?php $slots = new OxPHP\Shared\Counter(); $cap = 100; // Claim a slot only while under the cap. do { $cur = $slots->get(); if ($cur >= $cap) { // full — reject break; } } while (!$slots->compareAndSet($cur, $cur + 1));

批量累加

php
<?php $bytes = new OxPHP\Shared\Counter(); // Sum a batch in PHP, then one atomic add (one FFI call). $deltas = array_map(fn ($req) => strlen($req['body']), $batch); $newTotal = $bytes->add(array_sum($deltas));

语义与注意事项

set() 会先返回前一个值,然后再存入——整个过程是原子的。set(0) 是快照并清零(LongAdder::sumThenReset)模式;set($n) 则设定任意新的起始值。

Relaxed 顺序

每个操作都是原子的,但 Counter 不会发布其他内存。如果读取方必须观察到写入方在递增该整数之前所写的数据,那就是同步——请使用带 Ordering::Release/AcquireShared\Atomic

compareAndSet 采用 Relaxed/Relaxed,不接受任何顺序参数。对于基于计数器自身取值做出的决策(上限、下限、按值占用),它是正确的。而需要发布其他状态的 CAS 则属于 Shared\Atomic

溢出会回绕

加过 INT_MAX 后会回绕到 INT_MIN。对于以每秒数千次的速率连续运行数月的计数器,请将其值保持在数十万亿的范围内,或定期重置。

**不支持小数值。**为了得到浮点精度的平均值而在统计字节数?请分别用两个 Counter 记录分子和分母,在读取时再做除法。

异常

异常 抛出场景
StaleHandleException 在注册表条目已被驱逐的句柄上调用任何方法。
UninitializedException 在尚未完成 __construct 的包装器上调用 id()

对于溢出或极端值,Counter 永远不会抛出异常——它会回绕。

可观测性

完整介绍参见 Shared Observability。快速参考:

  • GET /__ox_shared/entry?id=N 会暴露 { value, type: "Counter" }
  • Prometheus 的 oxphp_shared_counter_value{counter_id="…"} gauge 会跟踪当前值。
  • 注册表级别的计数器(oxphp_shared_operations_totaloxphp_shared_objects_total)通过 type="Counter" 标签覆盖 Counter。

何时不该使用

  • **浮点数或小数。**请使用一对 Counter(分子/分母),或 Shared\Mutex<array{total_cents: int, count: int}>
  • **需要丰富上下文的非数值事件。**如果你需要将 {count, last_actor, last_reason} 绑定到一个键上,请选用 Shared\MapShared\Mutex
  • **跨主机汇总。**Counter 仅限进程内使用。对于多主机聚合,请使用指标管道(Prometheus + rate(),或集中式的 Redis INCR)。
  • **持久性。**服务器停止时 Counter 的状态会消失。如果总数必须在重启后保留,请将快照持久化到别处。

相关内容

  • 共享状态——概述与迁移模式。
  • Shared\Atomic——通用的原子 int64,支持 CAS、swap 以及完整的内存顺序控制。
  • Shared\Map——当计数需要按键区分时(Map<string, Counter>)。
  • Shared\Flag——当值只是开/关时。
  • Shared\Mutex——当计数器必须与其他字段同步更新时。