Shared\Atomic

OxPHP\Shared\Atomic — это атомарное 64-битное знаковое целое, действующее в пределах всего процесса, с полным набором примитивных операций: load, store, swap, compareAndSet, а также fetchAdd/Sub/And/Or/Xor. Каждая операция выполняется без блокировок (lock-free), а порядок обращений к памяти задаётся явно и по умолчанию равен SeqCst.

Обзор

  • Атомарный примитив int64. Диапазон −9_223_372_036_854_775_808 … 9_223_372_036_854_775_807. При переполнении значение оборачивается по кругу.
  • Без блокировок (lock-free). Каждая операция компилируется в одну атомарную инструкцию процессора (load, store, xchg, cmpxchg, xadd и т. д.).
  • Порядок обращений к памяти выбираете вы. Передайте значение перечисления OxPHP\Shared\Ordering, когда нужен Relaxed / Acquire / Release / AcqRel / SeqCst. По умолчанию используется SeqCst, так что вызывающий код, которому это безразлично, получает самую строгую гарантию.

Когда использовать Atomic вместо Shared\Counter:

  • Конечные автоматыcompareAndSet для переходов idle → busy → done.
  • Отметки версий / счётчики поколенийfetchAdd(1) возвращает предыдущую версию; читатели могут использовать её для обнаружения гонок.
  • CAS-циклы — прочитайте значение через load, вычислите новое и повторяйте compareAndSet, пока он не выполнится успешно.
  • Маски битовых флаговfetchOr для установки, fetchAnd для сброса.

Counter — правильный инструмент для накопления (add); Atomic — правильный инструмент для произвольного атомарного состояния.

Справочник API

php
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.
  • Release (для сохранений) — работает в паре с загрузкой Acquire; записи, сделанные до этой операции, видны сторонам, выполняющим Acquire.
  • AcqRel (для операций «чтение-модификация-запись») — сочетает обе стороны: загрузку 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, поэтому вызывающий код, не задумывающийся о порядке, всё равно получает безопасное поведение. Недопустимая комбинация приводит к исключению OxPHP\Shared\InvalidOrderingException ещё до вызова FFI.

Подробный разбор модели памяти C++/Rust смотрите в документации Rust по std::sync::atomic::Ordering.

Примеры

Конечный автомат через compareAndSet

php
<?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
<?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
<?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
<?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 возвращает предыдущее значение

fetchAdd возвращает предыдущее значение, а не новое. Это намеренно контрастирует с Counter::add, который возвращает новый итог. Разная абстракция — разное соглашение о возвращаемом значении: выбирайте класс, соответствующий нужной вам семантике.

Переполнение оборачивается по кругу

i64::MIN.fetchSub(1) даёт i64::MAX. Исключение не возбуждается.

По умолчанию порядок — SeqCst

SeqCst — самый безопасный и самый медленный выбор. Переходите к Acquire/Release/Relaxed только тогда, когда можете чётко объяснить, зачем.

Atomic хранит одно значение int64. Для составного состояния (несколько связанных полей) используйте Shared\Mutex.

Исключения

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

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

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

  • GET /__ox_shared/entry?id=N отдаёт { value, type: "Atomic" }.
  • Счётчики уровня всего реестра (oxphp_shared_operations_total, oxphp_shared_objects_total) охватывают Atomic через метку type="Atomic".

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

  • Составное состояние. Несколько полей, которые должны обновляться вместе → Shared\Mutex.
  • Подсчёт / накопление. Используйте Shared\Counter — его add, возвращающий новый итог, соответствует предметной области.
  • Числа с плавающей точкой или десятичные. Не поддерживаются; оберните структуру в Shared\Mutex или используйте пару счётчиков Counter (числитель / знаменатель).
  • Координация между хостами. Atomic работает только в пределах процесса. Для состояния на нескольких хостах используйте Redis, базу данных или конвейер метрик.
  • Долговечность. Состояние Atomic исчезает при остановке сервера. Сохраняйте снимки в другом месте, если значение должно переживать перезапуски.

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

  • Разделяемое состояние — обзор и паттерны миграции.
  • Shared\Counter — когда значение является предметным накопителем.
  • Shared\Mutex — когда состояние охватывает больше одного int64.
  • Shared\Flag — когда значение — это просто вкл/выкл.