Menu

Reference / Async

TaskGroup

finalclass
final class TaskGroup

A structured-concurrency scope (a "nursery").

Every spawn() attaches to the innermost scope open in the calling task. The scope does not close until all of its children have settled, so a task can never outlive the code that opened the scope. The first child failure cancels the siblings and propagates out of the scope once they have been joined. Cancellation is recursive: cancelling a scope cancels the scopes nested in it.

Open a scope with group() or TaskGroup::run(). The scope lives on the task, not in a global stack, so scopes opened by concurrently running tasks never adopt each other's children. async() opens an implicit root scope, which is why even a top-level spawn() is joined.

Async\group(function (TaskGroup $g) {
    $g->spawn(fn() => work(1));
    $g->spawn(fn() => work(2));
}); // returns only once both children are done

Example

use Async\TaskGroup;
use function Async\async;
use function Async\delay;
use function Async\group;

async(function () {
    // group() opens a child scope. It returns the body's value, but only
    // after every task spawned inside has settled.
    $answer = group(function (TaskGroup $g) {
        $g->spawn(function () {
            delay(0.02);
            echo "second child done\n";
        });
        $g->spawn(function () {
            delay(0.01);
            echo "first child done\n";
        });
        echo "body finished\n";
        return 42;
    });

    echo "group returned $answer\n";
});

Output

body finished
first child done
second child done
group returned 42

Example

use Async\CancelledException;
use Async\TaskGroup;
use function Async\async;
use function Async\delay;

try {
    async(function () {
        TaskGroup::run(function (TaskGroup $g) {
            $g->spawn(function () {
                try {
                    delay(10.0);
                } catch (CancelledException $e) {
                    echo "sibling cancelled\n";
                    throw $e;
                }
            });
            $g->spawn(function () {
                delay(0.01);
                throw new RuntimeException("child failed");
            });
        });
        echo "not reached\n";
    });
} catch (RuntimeException $e) {
    // The first failure cancelled the sibling, the scope joined it, and the
    // original exception came out of the scope.
    echo "caught: ", $e->getMessage(), "\n";
}

Output

sibling cancelled
caught: child failed

See also: group, Task, CancellationToken

Methods

token()The read-only cancellation handle for this scope.
run()Open a scope, run $body($group), then wait for every child before returning.
spawn()Spawn a child task into this scope and return a handle to await.
cancel()Cancel every live child of this scope.

token()#source

function token(): CancellationToken

The read-only cancellation handle for this scope.

Pass the token to code that should observe cancellation without being able to cause it.

Returns

CancellationToken — a view of this scope

See also: CancellationToken

run()#source

static function run(callable $body): mixed

Open a scope, run $body($group), then wait for every child before returning.

Returns the body's value. If the body throws or a child fails, the remaining children are cancelled and joined first, and then the first failure is thrown; a CancelledException that cancelled the scope itself is never swallowed. group() is the everyday spelling of this.

Parameters

$body callable
receives the new TaskGroup; its return value is returned

Returns

mixed — the body's return value

Throws

\Throwable
the first failure in the scope

Example

use Async\TaskGroup;
use function Async\async;
use function Async\delay;
use function Async\group;

async(function () {
    // group() opens a child scope. It returns the body's value, but only
    // after every task spawned inside has settled.
    $answer = group(function (TaskGroup $g) {
        $g->spawn(function () {
            delay(0.02);
            echo "second child done\n";
        });
        $g->spawn(function () {
            delay(0.01);
            echo "first child done\n";
        });
        echo "body finished\n";
        return 42;
    });

    echo "group returned $answer\n";
});

Output

body finished
first child done
second child done
group returned 42

Example

use Async\CancelledException;
use Async\TaskGroup;
use function Async\async;
use function Async\delay;

try {
    async(function () {
        TaskGroup::run(function (TaskGroup $g) {
            $g->spawn(function () {
                try {
                    delay(10.0);
                } catch (CancelledException $e) {
                    echo "sibling cancelled\n";
                    throw $e;
                }
            });
            $g->spawn(function () {
                delay(0.01);
                throw new RuntimeException("child failed");
            });
        });
        echo "not reached\n";
    });
} catch (RuntimeException $e) {
    // The first failure cancelled the sibling, the scope joined it, and the
    // original exception came out of the scope.
    echo "caught: ", $e->getMessage(), "\n";
}

Output

sibling cancelled
caught: child failed

See also: group

spawn()#source

function spawn(callable $fn, mixed ...$args): Task

Spawn a child task into this scope and return a handle to await.

The task is owned by this scope and joined when the scope closes. The task starts when the calling task next suspends. Spawning into a scope that is already cancelled starts the child, which is cancelled at its first suspend point.

Parameters

$fn callable
the task body
...$args mixed
passed to $fn

Returns

Task — the handle; await() it or let the scope join it

Example

use Async\TaskGroup;
use function Async\async;
use function Async\delay;
use function Async\group;

async(function () {
    // group() opens a child scope. It returns the body's value, but only
    // after every task spawned inside has settled.
    $answer = group(function (TaskGroup $g) {
        $g->spawn(function () {
            delay(0.02);
            echo "second child done\n";
        });
        $g->spawn(function () {
            delay(0.01);
            echo "first child done\n";
        });
        echo "body finished\n";
        return 42;
    });

    echo "group returned $answer\n";
});

Output

body finished
first child done
second child done
group returned 42

See also: spawn

cancel()#source

function cancel(): void

Cancel every live child of this scope. The write half of cancellation.

Handlers registered with CancellationToken::onCancel() run first, synchronously. Then each parked child is taken off whatever it waits on and gets a CancelledException at its suspend point; a child that opened scopes of its own unwinds through their joins, so cancellation is recursive. A child that calls cancel() on its own scope is cancelled too. Cancelling twice does nothing, and the call does not wait: the scope still joins its children when it closes. Cancelling a scope is not a failure of the scope.

Example

use Async\CancelledException;
use Async\TaskGroup;
use function Async\async;
use function Async\delay;
use function Async\group;

async(function () {
    group(function (TaskGroup $g) {
        $token = $g->token();
        $token->onCancel(fn() => print("onCancel hook ran\n"));

        // A task that suspends is interrupted with CancelledException,
        // which extends \Error: catch (\Exception) cannot swallow it.
        $g->spawn(function () {
            try {
                delay(10.0);
            } catch (CancelledException $e) {
                echo "sleeper cancelled\n";
                throw $e;
            }
        });

        // A loop that never suspends polls the token instead.
        $g->spawn(function () use ($g, $token) {
            for ($step = 1; $step <= 5; $step++) {
                if ($step === 3) {
                    $g->cancel();   // the write half; $token is the read-only view
                }
                if ($token->isCancelled()) {
                    echo "loop stopped at step $step\n";
                    return;
                }
                echo "step $step\n";
            }
        });
    });
    echo "scope joined\n";
});

Output

step 1
step 2
onCancel hook ran
loop stopped at step 3
sleeper cancelled
scope joined

See also: CancellationToken, Task::cancel

View source