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
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
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
$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
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
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 возвращает предыдущее значение, а не новое. Это намеренно контрастирует с Counter::add, который возвращает новый итог. Разная абстракция — разное соглашение о возвращаемом значении: выбирайте класс, соответствующий нужной вам семантике.
i64::MIN.fetchSub(1) даёт i64::MAX. Исключение не возбуждается.
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 — когда значение — это просто вкл/выкл.