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; rzucaContentionException, jeśli blokada jest zajęta.withLockTimeout($fn, int $ms)— ograniczone oczekiwanie; rzucaOperationTimeoutExceptionpo 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
DeadlockExceptionzamiast się zawiesić.
Dokumentacja API
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:
Mutexstosuje styl wyjątkowy — rzadka ścieżka jest tą wyjątkową.Channelstosuje 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
$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:
$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
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
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
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
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
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.
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ę.
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=Nudostę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\CounterlubShared\Flag— oba są bezblokadowe i tańsze. - Długo trwająca praca. Nie trzymaj muteksu przez wejście/wyjście,
sleepczy oczekiwania Fiberów. Użyj zamiast tego wzorca producent/konsument opartego naShared\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.