Shared\Counter

OxPHP\Shared\Counter — это атомарное 64-битное знаковое целое, действующее в пределах всего процесса и специализированное на накоплении: подсчёте событий, суммировании дельт, скользящих оконных итогах. Все операции неблокирующие; два воркера, выполняющих сложение конкурентно, никогда не теряют ни одного тика.

Для произвольного атомарного состояния, которое должно синхронизировать другую память (конечные автоматы, метки версий, seqlock'и, битовые маски флагов), используйте Shared\Atomic.

Обзор

  • Атомарный int64. Диапазон −9_223_372_036_854_775_808 … 9_223_372_036_854_775_807. Переполнение заворачивается по кругу.
  • Неблокирующий (lock-free). 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"; });

Оконный сброс (rollover)

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 не публикует другую память. Если читатель должен увидеть данные, которые писатель записал до инкремента целого, — это синхронизация, используйте Shared\Atomic с Ordering::Release/Acquire.

compareAndSet работает в режиме Relaxed/Relaxed и не принимает аргументов упорядочения. Он корректен для решений, принимаемых по собственному значению счётчика (потолок, пол, захват по значению). CAS, публикующий другое состояние, — это задача для Shared\Atomic.

Переполнение заворачивается

Сложение за пределами INT_MAX заворачивается к INT_MIN. Для счётчиков, работающих месяцами со скоростью в тысячи операций в секунду, держите значение в диапазоне десятков триллионов или периодически сбрасывайте его.

Никаких дробных значений. Считаете байты для усреднений с точностью float? Отслеживайте числитель (Counter) и знаменатель (Counter) по отдельности и делите при чтении.

Исключения

Исключение Кем возбуждается
StaleHandleException Любой метод дескриптора, запись которого была вытеснена из реестра.
UninitializedException id() на обёртке, ещё не завершившей __construct.

Счётчики никогда не выбрасывают исключения при переполнении или экстремальных значениях — они заворачиваются по кругу.

Наблюдаемость

Полный обзор см. в Наблюдаемость разделяемого состояния. Краткая справка:

  • GET /__ox_shared/entry?id=N возвращает { value, type: "Counter" }.
  • Prometheus-датчик (gauge) oxphp_shared_counter_value{counter_id="…"} отслеживает текущее значение.
  • Счётчики уровня всего реестра (oxphp_shared_operations_total, oxphp_shared_objects_total) покрывают Counter через метку type="Counter".

Когда не стоит использовать

  • Числа с плавающей точкой или десятичные. Используйте пару Counter'ов (числитель / знаменатель) или Shared\Mutex<array{total_cents: int, count: int}>.
  • Нечисловые события, требующие богатого контекста. Если вам нужно связать {count, last_actor, last_reason} с одним ключом, обратитесь к Shared\Map или Shared\Mutex.
  • Итоги между хостами. Counter существует только в пределах процесса. Для агрегации по нескольким хостам используйте конвейер метрик (Prometheus + rate() или центральный INCR в Redis).
  • Долговечность. Состояние Counter исчезает при остановке сервера. Сохраняйте снимки в другом месте, если итог должен пережить перезапуски.

Связанные материалы

  • Разделяемое состояние — обзор и паттерны миграции.
  • Shared\Atomic — универсальный атомарный int64 с CAS, swap и полным контролем упорядочения памяти.
  • Shared\Map — когда счётчики привязаны к ключам (Map<string, Counter>).
  • Shared\Flag — когда значение — это просто вкл/выкл.
  • Shared\Mutex — когда счётчик должен обновляться синхронно с другими полями.