Shared\Mutex

OxPHP\Shared\Mutex to obejmująca cały proces blokada wzajemnego wykluczania, która opakowuje przechowywaną wartość. Nigdy nie dotykasz blokady bezpośrednio. Zamiast tego przekazujesz domknięcie do jednego z trzech wariantów metody, a środowisko uruchomieniowe trzyma blokadę przez czas trwania domknięcia i zwalnia ją nawet wtedy, gdy domknięcie rzuci wyjątek.

Przegląd

  • Chroni wartość, a nie tylko sekcję. Opakowana wartość jest przekazywana do Twojego domknięcia przez referencję, więc bezpośrednia mutacja wewnątrz domknięcia zostaje zatwierdzona z powrotem, gdy domknięcie zwróci wartość w normalny sposób.
  • Trzy jawne polityki oczekiwania zamiast jednego przeciążonego ?float $timeout:
    • withLock($fn) — blokuje w nieskończoność (lub do momentu anulowania Fibera żądania).
    • tryWithLock($fn) — nieblokujące; rzuca ContentionException, jeśli blokada jest zajęta.
    • withLockTimeout($fn, int $ms) — ograniczone oczekiwanie; rzuca OperationTimeoutException po upływie terminu.
  • Wyjątki PHP propagują się swobodnie. Jeśli Twoje domknięcie rzuci zwykły wyjątek PHP, blokada zostaje zwolniona, a wyjątek wypływa w górę. Mutex nie jest uszkodzony — częściowa mutacja jest akceptowalna; to wywołujący odpowiada za przywrócenie niezmienników.
  • Panika Rusta uszkadza mutex. Jeśli panika Rusta przekroczy granicę FFI (błąd serwera), mutex wchodzi w trwały stan uszkodzenia i każde kolejne przejęcie rzuca CorruptedMutexException. Nie ma API odzyskiwania — porzuć instancję i utwórz nową.
  • Zapobiega zakleszczeniom. Ponowne wejście w ten sam mutex na tym samym wątku (w tym poprzez zagnieżdżone wywołania asynchroniczne przechwycone na tym wątku) zgłasza DeadlockException zamiast się zawiesić.

Dokumentacja API

php
namespace OxPHP\Shared; final class Mutex implements Shareable { public function __construct(mixed $initial = null); public function withLock(callable $fn): mixed; public function tryWithLock(callable $fn): mixed; public function withLockTimeout(callable $fn, int $ms): mixed; public function id(): int; }

Sygnatura domknięcia to function (mixed &$value): mixed$value jest przekazywane przez referencję, więc mutujesz je w miejscu. Normalna wartość zwracana przez domknięcie jest przekazywana do wywołującego withLock / tryWithLock / withLockTimeout. Ścieżka zwrotna obsługuje skalary, null oraz instancje Shared\* (string, int, float, bool, byte-string, null oraz dowolny uchwyt implementujący OxPHP\Shared\Shareable). Zwrócenie tablicy PHP zgłasza OxPHP\Shared\TypeException — ten przypadek nie jest jeszcze obsługiwany i jest śledzony osobno. Aby wynieść w górę ustrukturyzowany stan tablicowy, albo zmutuj &$value w miejscu i odczytaj je ponownie po wywołaniu, albo umieść to, czego potrzebujesz, w zmiennej use (&$captured).

Metoda Zachowanie
withLock($fn) Blokuje do przejęcia, następnie uruchamia domknięcie. Na zawsze / anulowanie.
tryWithLock($fn) Nieblokujące. Rzuca ContentionException, jeśli zajęte.
withLockTimeout($fn, $ms) Ograniczone oczekiwanie. Wymagane $ms > 0. Rzuca OperationTimeoutException po upływie terminu.
id() Identyfikator w rejestrze; przydatny do logowania / obserwowalności.

$ms to ściśle dodatnia liczba całkowita milisekund. Zero, wartości ujemne, nie-int oraz brak wartości zgłaszają OxPHP\Shared\TypeException na moście — zamiast próbować wyrazić te polityki przez $ms, wywołaj withLock (na zawsze) lub tryWithLock (nieblokujące).

Dlaczego Mutex rzuca wyjątki, a Channel zwraca Results

Rywalizacja i przekroczenie limitu czasu to rzadkie zdarzenia dla dobrze zaprojektowanego muteksu (blokady powinny być trzymane przez krótkie sekcje krytyczne; utrzymująca się rywalizacja to zapach kodu). Są to rutynowe zdarzenia dla kanału (dyspozytor typu fan-out widzi Full/Closed/Timeout w każdym zajętym cyklu). Dlatego:

  • Mutex stosuje styl wyjątkowy — rzadka ścieżka jest tą wyjątkową.
  • Channel stosuje styl oparty na Result — częsta ścieżka pozostaje poza maszynerią throw/catch.

Jeśli łapiesz się na owijaniu każdego withLock w try { … } catch (ContentionException) { … }, używasz niewłaściwego prymitywu. Sięgnij po Shared\Channel dla obciążeń w kształcie kolejki albo Shared\Counter / Shared\Flag dla atomowości pojedynczej wartości.

Ten sam strukturalny powód wyjaśnia, dlaczego Pool::tryAcquire() może zwrócić null tam, gdzie Mutex::tryWithLock() rzuca wyjątek. Pool jest uchwyt-first: tryAcquire(): ?Handle niesie „nasycenie” jako null, a Handle sam nigdy nie jest wartością użytkownika, więc nie ma dwuznaczności. Mutex jest wyłącznie domknięciowy — celowo nigdy nie oddaje strażnika blokady z powrotem do PHP (aby trzymana blokada nie mogła wyciec poza domknięcie), co nie pozostawia żadnego obiektu do zwrócenia jako nullable, a własny wynik mixed domknięcia może już być null. Bez wolnej wartości wartowniczej rywalizacja ujawnia się jako ContentionException. Dwie powierzchnie try* rozchodzą się z powodu tego, co każdy typ może oddać, a nie z preferencji stylistycznej.

Przykłady

Atomowa aktualizacja wielu pól

Counter wystarcza, gdy wartość to pojedyncza liczba całkowita. Mutex wygrywa, gdy kilka pól musi zaktualizować się w jednym kroku:

php
<?php $stats = new OxPHP\Shared\Mutex(['hits' => 0, 'bytes' => 0]); $stats->withLock(function (array &$s) use ($responseBytes) { $s['hits'] += 1; $s['bytes'] += $responseBytes; });

Inny worker obserwujący wartość odczytuje oba pola w jednej sekcji krytycznej:

php
$snapshot = ['hits' => 0, 'bytes' => 0]; $stats->withLock(function (array &$s) use (&$snapshot) { $snapshot = $s; }); // $snapshot sees both fields from the same update or neither — never the // bumped 'hits' without the matching 'bytes'. (We capture through use(&$x) // because the closure's own return is currently scalar-only — see the // closure-signature note above.)

Nieblokujące sondowanie + degradacja

php
<?php use OxPHP\Shared\{Mutex, ContentionException}; $budget = new Mutex(['tokens' => 100, 'refill_at' => time()]); try { $budget->tryWithLock(function (array &$b) { if ($b['tokens'] <= 0) { // No tokens — leave state untouched. return; } $b['tokens'] -= 1; }); } catch (ContentionException) { // Lock held by another worker — shed the request instead of queuing. http_response_code(503); return; }

Przejęcie z limitem czasu

php
<?php use OxPHP\Shared\{Mutex, OperationTimeoutException}; $counter = new Mutex(0); try { // Return value is scalar — int $next — so the closure return is forwarded. $next = $counter->withLockTimeout(function (int &$c) { $c += 1; return $c; }, ms: 5000); } catch (OperationTimeoutException) { // Someone else held the lock longer than 5s. }

Nazwane argumenty są zalecane: ms: 5000 czyta się jako „5000 milisekund” bez konieczności pamiętania przez czytelnika kolejności parametrów.

Przechwyć wszystkie warunki współbieżności w jednym miejscu

OperationTimeoutException, ContentionException oraz DeadlockException — wszystkie rozszerzają OxPHP\Async\AsyncException. Jeden catch obejmuje każdy wynik współbieżności na powierzchniach Shared* oraz Async*:

php
<?php use OxPHP\Async\AsyncException; try { $state->withLockTimeout($fn, 100); } catch (AsyncException) { // timeout, contention, deadlock, or any await-related concurrency error }

Katastroficzne odzyskiwanie z uszkodzonego muteksu

Panika Rusta podczas wywołania domknięcia (błąd serwera, a nie cokolwiek, co zrobił kod PHP) pozostawia blokadę w trwałym stanie uszkodzenia. Nie ma odpowiednika clearPoison(), więc porzuć instancję:

php
<?php use OxPHP\Shared\{Mutex, CorruptedMutexException}; try { $state->withLock($fn); } catch (CorruptedMutexException) { // Old instance is dead. Recreate from the persistent source of truth. $state = new Mutex($initialState); }

Semantyka i pułapki

Domknięcie działa z trzymaną blokadą

Utrzymuj je krótkim. Nie wywołuj sleep, nie blokuj na wejściu/wyjściu sieciowym i nie wchodź ponownie w inne typy Shared*, które mogłyby oddzwonić do tego muteksu.

Wyjątki PHP nie uszkadzają już blokady

To celowa zmiana względem poprzedniej polityki Poisoned-przy-każdym-rzuceniu: polityka częściowej mutacji brzmi teraz „to wywołujący odpowiada za przywrócenie niezmienników”. Jeśli potrzebujesz wzorca próbnego obliczenia bez mutacji, wykonaj je poza muteksem i wywołaj withLock tylko po to, by zatwierdzić finalną wartość.

Przechowywana wartość jest quasi-skalarna. Działają stringi, inty, floaty, wartości logiczne, null oraz zagnieżdżone tablice tych typów. Obiekty, domknięcia i zasoby zgłaszają TypeException.

Zwrot domknięcia obejmuje skalary, null oraz instancje Shared\*; tablice nie są jeszcze obsługiwane. Przechowywana wartość nadal może być tablicą (mutuj ją przez &$value), ale własna ścieżka zwrotu domknięcia akceptuje string/int/float/bool/null/byte-string oraz dowolny uchwyt OxPHP\Shared\Shareable. Zwrócenie tablicy PHP zgłasza OxPHP\Shared\TypeException. Obejście dla tablic: przechwyć do zmiennej use (&$x) albo odczytaj stan przez kolejne withLock, które zwraca skalarną projekcję.

Ponowne wejście na tym samym wątku rzuca DeadlockException

Użyj innego muteksu lub przebuduj kod. Ponowne wejście na tym samym wątku to błąd, a nie funkcja.

Anulowanie Fibera propaguje się jako Async\AsyncException. withLock przerwane przez anulowanie żądania zgłasza ten wyjątek, a blokada zostaje czysto zwolniona.

Wyjątki

Wyjątek Rodzic Zgłaszany przez
ContentionException Async\AsyncException tryWithLock na zajętej blokadzie.
OperationTimeoutException Async\AsyncException Upłynął termin withLockTimeout.
DeadlockException Async\AsyncException Ponowne wejście na tym samym wątku lub wykryty cykl wait-for.
CorruptedMutexException Shared\SharedException Wcześniejsze wywołanie domknięcia zawiesiło się przez panikę Rusta; mutex jest bezużyteczny.
TypeException Shared\SharedException Konstruktor lub argument $ms naruszył swój kontrakt typu.
StaleHandleException Shared\SharedException Wywołanie metody na uchwycie, którego wpis w rejestrze został usunięty.
UninitializedException Shared\SharedException id() na wrapperze, który nie zakończył __construct.

Obserwowalność

Zobacz Obserwowalność stanu współdzielonego. Szybkie odniesienia:

  • GET /__ox_shared/entry?id=N udostępnia { type: "Mutex", corrupted, waiters, last_acquire_ms, held_by_thread }.
  • Metryki Prometheus na instancję:
    • oxphp_shared_mutex_waiters{mutex_id="…"} — bieżąca liczba oczekujących.
    • oxphp_shared_mutex_acquires_total{mutex_id="…"} — przejęcia w całym cyklu życia.
    • oxphp_shared_mutex_contended_total{mutex_id="…"} — przejęcia, które musiały poczekać.
    • oxphp_shared_mutex_corrupted{mutex_id="…"} — 0 / 1 (zmieniona nazwa z _poisoned).

Kiedy nie używać

  • Pojedyncza atomowa wartość. Jeśli chroniona wartość to jeden int lub jeden bool, użyj Shared\Counter lub Shared\Flag — oba są bezblokadowe i tańsze.
  • Długo trwająca praca. Nie trzymaj muteksu przez wejście/wyjście, sleep czy oczekiwania Fiberów. Użyj zamiast tego wzorca producent/konsument opartego na Shared\Channel.
  • Gorąca ścieżka o wysokiej rywalizacji. Jeśli każde żądanie musi przejąć ten sam mutex, zserializowałeś swoją przepustowość. Podziel stan (np. Shared\Map<tenant_id, Mutex>) albo wstępnie agreguj w lokalnych zmiennych per worker i okresowo je opróżniaj.
  • Wzajemne wykluczanie między hostami. Wyłącznie w obrębie procesu. Do koordynacji wielohostowej użyj rozproszonej blokady (Redis SET NX, etcd).

Migracja z poprzedniego API

Było Jest
$m->with($fn) (na zawsze) $m->withLock($fn)
$m->with($fn, $secs) $m->withLockTimeout($fn, $ms) z $ms w milisekundach
$m->tryWith($fn)null przy rywalizacji $m->tryWithLock($fn) → rzuca ContentionException
$m->isPoisoned() / $m->clearPoison() usunięte; wyjątki PHP nie uszkadzają już muteksu
PoisonedException (ścieżka paniki Rusta) CorruptedMutexException (brak publicznego API czyszczenia)
Shared\TimeoutException Shared\OperationTimeoutException (teraz rozszerza Async\AsyncException)
DeadlockException extends Shared\TimeoutException DeadlockException extends Async\AsyncException

Sygnatura domknięcia również zmieniła się z function (mixed $value): mixed (zwrot-aby-zatwierdzić) na function (mixed &$value): mixed (mutacja przez referencję, normalny zwrot jest wartością domknięcia, a nie nowym stanem). Jeśli domknięcie nic nie zwraca, przechowywana wartość zachowuje to, co pozostawiła w niej mutacja przez referencję. Jedno wcześniej istniejące ograniczenie przenosi się dalej: zwracana wartość domknięcia musi być skalarem (string / int / float / bool / null / byte-string) lub uchwytem Shared\* — zwrócenie tablicy PHP rzuca OxPHP\Shared\TypeException. Przechowywana wartość nadal może być tablicą; mutuj ją przez &$value i użyj use (&$x), aby wynieść ustrukturyzowane dane w górę.

Powiązane

  • Stan współdzielony — przegląd i model myślowy.
  • Shared\Counter — gdy chroniony stan to jedna liczba całkowita.
  • Shared\Flag — gdy chroniony stan to jeden bool.
  • Shared\Channel — gdy potrzebujesz oczekiwania + przekazania zamiast wzajemnego wykluczania (i chcesz zwrotów w stylu Result zamiast stylu wyjątkowego).
  • Shared\Map — podziel Mutex na klucz, aby uniknąć globalnej rywalizacji.