Shared\Atomic

OxPHP\Shared\Atomic est un entier signé atomique de 64 bits à l'échelle du processus, doté de toute la surface primitive : load, store, swap, compareAndSet, ainsi que fetchAdd/Sub/And/Or/Xor. Chaque opération est sans verrou, et l'ordonnancement mémoire est explicite et vaut SeqCst par défaut.

Vue d'ensemble

  • Primitive int64 atomique. Plage −9_223_372_036_854_775_808 … 9_223_372_036_854_775_807. Le dépassement boucle (wrap).
  • Sans verrou. Chaque opération se compile en une seule instruction atomique du CPU (load, store, xchg, cmpxchg, xadd, etc.).
  • L'ordonnancement mémoire est à votre main. Passez une valeur de l'enum OxPHP\Shared\Ordering lorsque vous avez besoin de Relaxed / Acquire / Release / AcqRel / SeqCst. La valeur par défaut est SeqCst, si bien que les appelants que cela n'intéresse pas obtiennent la garantie la plus forte.

Quand utiliser Atomic plutôt que Shared\Counter :

  • Machines à étatscompareAndSet pour idle → busy → done.
  • Estampilles de version / compteurs de générationfetchAdd(1) renvoie la version précédente ; les lecteurs peuvent s'en servir pour détecter des accès concurrents.
  • Boucles CAS — lire avec load, calculer la nouvelle valeur, réessayer compareAndSet jusqu'à ce qu'elle réussisse.
  • Masques de bits (bitflags)fetchOr pour poser un bit, fetchAnd pour l'effacer.

Counter est l'outil adapté à l'accumulation (add) ; Atomic est l'outil adapté à un état atomique arbitraire.

Référence de l'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; }
Méthode Renvoie Cas d'usage
load valeur courante Lire la valeur avec l'ordonnancement choisi.
store void Écrire une nouvelle valeur, en abandonnant l'ancienne.
swap précédente Remplacement atomique ; swap(0) est le motif « instantané puis remise à zéro ».
compareAndSet échangé ? Transitions optimistes et boucles CAS.
fetchAdd/Sub précédente Compteurs de génération, compteurs bornés via CAS, deltas.
fetchAnd/Or/Xor précédente Masques de bits : poser, effacer, basculer.
id id de registre Journalisation, traçage, corrélation /__ox_shared/entry?id=….

Ordonnancement mémoire

Petit rappel :

  • Relaxed — atomicité seule, aucun ordre relatif aux autres accès mémoire.
  • Acquire (chargements) — s'apparie à un store Release ; les lectures effectuées après cette opération observent les écritures que le releaser avait achevées.
  • Release (stores) — s'apparie à un load Acquire ; les écritures effectuées avant cette opération sont visibles pour les acquéreurs.
  • AcqRel (lecture-modification-écriture) — les deux moitiés : un load Acquire et un store Release.
  • SeqCst — un ordre total global unique sur toutes les opérations SeqCst.

Chaque opération n'accepte que les ordonnancements qui ont un sens pour elle :

Opération Autorisé
load Relaxed, Acquire, SeqCst
store Relaxed, Release, SeqCst
swap, fetchAdd, fetchSub, fetchAnd, fetchOr, fetchXor n'importe lequel
compareAndSet success n'importe lequel
compareAndSet failure Relaxed, Acquire, SeqCst

La valeur par défaut est Ordering::SeqCst partout, si bien que les appelants qui ne se soucient pas de l'ordonnancement obtiennent tout de même un comportement sûr. Une combinaison invalide lève OxPHP\Shared\InvalidOrderingException avant l'appel FFI.

Pour approfondir le modèle mémoire C++/Rust, voir la documentation de Rust std::sync::atomic::Ordering.

Exemples

Machine à états via 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; }

Compteur de génération / estampille de version

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);

Mise à jour optimiste via boucle 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));

Masque de bits (bitflag)

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(); }

Sémantique et pièges

fetchAdd renvoie la valeur précédente

fetchAdd renvoie la valeur précédente, pas la nouvelle. Ce choix contraste délibérément avec Counter::add, qui renvoie le nouveau total. Abstraction différente, convention de retour différente : choisissez la classe qui correspond à la sémantique que vous visez.

Le dépassement boucle

i64::MIN.fetchSub(1) donne i64::MAX. Aucune exception n'est levée.

L'ordonnancement par défaut est SeqCst

SeqCst est le choix le plus sûr et le plus lent. Ne descendez à Acquire/Release/Relaxed que lorsque vous pouvez expliquer pourquoi.

Un Atomic contient un seul int64. Pour un état composé (plusieurs champs couplés), utilisez Shared\Mutex.

Exceptions

Exception Levée par
StaleHandleException Toute méthode sur un handle dont l'entrée de registre a été évincée.
UninitializedException id() sur un wrapper qui n'a pas terminé son __construct.
InvalidOrderingException Une opération reçoit un ordonnancement mémoire invalide pour elle.

Observabilité

Voir Observabilité de l'état partagé pour le tour complet. Références rapides :

  • GET /__ox_shared/entry?id=N expose { value, type: "Atomic" }.
  • Les compteurs à l'échelle du registre (oxphp_shared_operations_total, oxphp_shared_objects_total) couvrent Atomic via le label type="Atomic".

Quand ne pas l'utiliser

  • État composé. Plusieurs champs qui doivent être mis à jour ensemble → Shared\Mutex.
  • Comptage / accumulation. Utilisez Shared\Counter — son add qui renvoie le nouveau total colle au domaine.
  • Flottants ou décimaux. Non pris en charge ; encapsulez une structure dans Shared\Mutex, ou associez deux Counters (numérateur / dénominateur).
  • Coordination inter-hôtes. Atomic est uniquement intra-processus. Pour un état multi-hôtes, utilisez Redis, une base de données ou un pipeline de métriques.
  • Durabilité. L'état d'un Atomic s'évapore à l'arrêt du serveur. Persistez les instantanés ailleurs si la valeur doit survivre aux redémarrages.

Voir aussi