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\Orderinglorsque vous avez besoin deRelaxed/Acquire/Release/AcqRel/SeqCst. La valeur par défaut estSeqCst, 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 à états —
compareAndSetpouridle → busy → done. - Estampilles de version / compteurs de génération —
fetchAdd(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éessayercompareAndSetjusqu'à ce qu'elle réussisse. - Masques de bits (bitflags) —
fetchOrpour poser un bit,fetchAndpour 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
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
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
$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
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
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, 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.
i64::MIN.fetchSub(1) donne i64::MAX. Aucune exception n'est levée.
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=Nexpose{ value, type: "Atomic" }.- Les compteurs à l'échelle du registre (
oxphp_shared_operations_total,oxphp_shared_objects_total) couvrent Atomic via le labeltype="Atomic".
Quand ne pas l'utiliser
- État composé. Plusieurs champs qui doivent être mis à jour ensemble →
Shared\Mutex. - Comptage / accumulation. Utilisez
Shared\Counter— sonaddqui 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
- État partagé — vue d'ensemble et schémas de migration.
- Shared\Counter — quand la valeur est un accumulateur métier.
- Shared\Mutex — quand l'état s'étend sur plus d'un int64.
- Shared\Flag — quand la valeur est juste on/off.