Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Tasks and Futures

Whim runs tasks on one event loop. A task may pause while it waits for a timer, a socket, a channel, or another task. Other ready tasks run during that pause. This is concurrency, not parallel work on several CPU cores.

Starting a task

Async\spawn() schedules a callable and returns a Future<T>.

use Whim\Async;

$future = Async\spawn::<int>(fn(): int {
  return 21 * 2;
});

$answer = $future->await();
assert!($answer == 42);

The task starts on a later event-loop turn. await() pauses the current task until the future succeeds or throws. If the task throws, await() throws the same error.

Code must observe every future. Await it, attach a result handler, return it, or call ignore() when the result does not matter. Dropping an unobserved failed future may raise UnhandledAwaitableException.

Future operations

Future<T> extends Promise<T> and provides:

  • await($cancellation): T waits for its result.
  • map($success): Future<U> changes a success value.
  • then($success, $failure): Future<U> handles either result.
  • catch($failure): Future<T|U> recovers from a failure.
  • always($callback): Future<T> runs after either result.
  • ignore(): static marks the result as intentionally unobserved.

These methods return a new future, except await() and ignore(). They do not change the source future.

use Whim\Async;

$length = Async\spawn::<string>(fn(): string => 'whim')
  ->map::<int>(fn(string $value): int => length!($value));

assert!($length->await() == 4);

Deferred results

Deferred<T> owns the write side of a future. Its consumer receives only the Future<T> interface.

use Whim\Async\Deferred;

$deferred = new Deferred::<int>();
$future = $deferred->getFuture();
$deferred->complete(42);
assert!($future->await() == 42);

Call complete($value) for success or error($error) for failure. A deferred may complete only once.

Waiting for many tasks

Async\all() awaits every keyed future and returns a dict with the same keys. It waits for all futures even when one fails. It throws the sole error or a CompositeException that contains all errors.

Async\concurrently() starts each supplied callable, then applies all(). Async\series() calls each callable in order without spawning tasks.

Async\first() returns or throws the first completed result. Async\any() returns the first successful result and throws CompositeException if every future fails. Both require at least one future. Futures that lose remain observable; neither function consumes or ignores their values.

Yielding and timers

  • Async\later() pauses until the next event-loop turn.
  • Async\sleep($duration, $cancellation) pauses for a duration.
  • Async\drain() runs scheduled work until no work remains.

Use later() to let ready work run without adding a time delay. Use sleep() for a real delay. Both may suspend the current task.

Task-local values

TaskLocal<T> stores one value per task. A new task starts with no value, even when the task that starts it has set one. Each TaskLocal object has its own storage.

use Whim\Async;
use Whim\Async\TaskLocal;

$requestId = new TaskLocal::<string>();
$requestId->set('main');

$future = Async\spawn::<null|string>(fn(): null|string => $requestId->get());
assert!($future->await() == null);
assert!($requestId->get() == 'main');

$requestId->clear();
assert!($requestId->get() == null);

Top-level code has its own task-local storage. get() returns null until the current task calls set(), and again after clear().

TaskGroup

TaskGroup tracks tasks that return void.

use Whim\Async\TaskGroup;

$group = new TaskGroup();
$group->defer(fn(): void {
  write_line!('first');
});
$group->defer(fn(): void {
  write_line!('second');
});
$group->awaitAll();

awaitAll() waits for every tracked task. It then throws the sole error or a CompositeException with all failures. A cancellation token cancels the wait, not the tasks already in the group.

WaitGroup

WaitGroup tracks work completed elsewhere. Call add() before starting one unit of work, call done() once it finishes, and call wait() to pause until the count reaches zero. done() throws if the count is already zero.

getCount() returns the current count. A cancellation token cancels only the wait.

Semaphore and Sequence

Semaphore<Tin, Tout> limits how many calls to one operation may run at once. Its constructor takes a positive limit and the operation. waitFor($input) waits for a slot, runs the operation, and releases the slot in a finally block.

Sequence<Tin, Tout> is a semaphore with a limit of one. It runs calls in order. Both types can report pending work, wait for a free slot, and cancel pending calls with a supplied error.