Menu

Reference / Async

Mutex

finalclass
final class Mutex

A mutual-exclusion lock for a critical section that spans a suspend point.

The scheduler is cooperative and single-threaded: nothing else runs between two suspend points, so an ordinary read-modify-write such as $shared->n = $shared->n + 1 is already atomic. There is no data race to protect against, and no need for atomics. What a Mutex protects is interleaving: a section that suspends in the middle (an await(), a delay(), a stream read) lets other tasks run and observe or change half-updated state.

$mu->withLock(function () use ($store) {
    $a = fetch();           // suspends: another task runs here
    $b = fetch();
    $store->write($a, $b);  // must not interleave with another writer
});

Waiting tasks are woken in arrival order. The lock is not reentrant: locking it again from the task that holds it throws LogicException instead of deadlocking. It is not tied to the owner on release: unlock() works from any task.

Example

use Async\Mutex;
use function Async\async;
use function Async\delay;
use function Async\spawn;

async(function () {
    $mu = new Mutex();

    // Each section suspends in the middle. Without the lock the two tasks
    // would interleave their steps.
    $work = function (string $name) use ($mu) {
        $mu->withLock(function () use ($name) {
            echo "$name: start\n";
            delay(0.01);
            echo "$name: end\n";
        });
    };

    $a = spawn(fn() => $work("A"));
    $b = spawn(fn() => $work("B"));
    $a->await();
    $b->await();

    // tryLock() never waits.
    $mu->lock();
    var_dump($mu->tryLock());
    $mu->unlock();
    var_dump($mu->isLocked());
});

Output

A: start
A: end
B: start
B: end
bool(false)
bool(false)

See also: Semaphore, Once

Methods

isLocked()Whether the lock is currently held.
lock()Take the lock, suspending until it is free.
tryLock()Take the lock only if it is free.
unlock()Release the lock and wake the first waiting task.
withLock()Hold the lock for the duration of $fn, releasing it however $fn ends.

isLocked()#source

function isLocked(): bool

Whether the lock is currently held.

Returns

bool — true while some task holds the lock

lock()#source

function lock(): void

Take the lock, suspending until it is free.

Throws

\LogicException
when the calling task already holds the lock
CancelledException
when the task is cancelled while waiting (the lock is not taken)

See also: withLock, tryLock

tryLock()#source

function tryLock(): bool

Take the lock only if it is free. Never suspends.

Returns

bool — true if the lock was taken; false if someone holds it

Example

use Async\Mutex;
use function Async\async;
use function Async\delay;
use function Async\spawn;

async(function () {
    $mu = new Mutex();

    // Each section suspends in the middle. Without the lock the two tasks
    // would interleave their steps.
    $work = function (string $name) use ($mu) {
        $mu->withLock(function () use ($name) {
            echo "$name: start\n";
            delay(0.01);
            echo "$name: end\n";
        });
    };

    $a = spawn(fn() => $work("A"));
    $b = spawn(fn() => $work("B"));
    $a->await();
    $b->await();

    // tryLock() never waits.
    $mu->lock();
    var_dump($mu->tryLock());
    $mu->unlock();
    var_dump($mu->isLocked());
});

Output

A: start
A: end
B: start
B: end
bool(false)
bool(false)

unlock()#source

function unlock(): void

Release the lock and wake the first waiting task.

Throws

\LogicException
when the lock is not held

See also: lock

withLock()#source

function withLock(callable $fn): mixed

Hold the lock for the duration of $fn, releasing it however $fn ends.

Parameters

$fn callable
called with no arguments while the lock is held

Returns

mixed — the value returned by $fn

Throws

\Throwable
whatever $fn throws, after the lock is released

Example

use Async\Mutex;
use function Async\async;
use function Async\delay;
use function Async\spawn;

async(function () {
    $mu = new Mutex();

    // Each section suspends in the middle. Without the lock the two tasks
    // would interleave their steps.
    $work = function (string $name) use ($mu) {
        $mu->withLock(function () use ($name) {
            echo "$name: start\n";
            delay(0.01);
            echo "$name: end\n";
        });
    };

    $a = spawn(fn() => $work("A"));
    $b = spawn(fn() => $work("B"));
    $a->await();
    $b->await();

    // tryLock() never waits.
    $mu->lock();
    var_dump($mu->tryLock());
    $mu->unlock();
    var_dump($mu->isLocked());
});

Output

A: start
A: end
B: start
B: end
bool(false)
bool(false)

View source