LEVIATHAN v962456e · 962456eee1

Library

Cancellation, timeouts and structured concurrency

Cancellation is an exception delivered at the next await; awaitTimeout turns a timeout into a None result; TaskGroup keeps tasks inside their scope.

since 0.1.0-alpha.1linuxwindows

Description

Cancellation is an exception delivered at park points, and only there. A running task is never interrupted in the middle of what it is doing. When a task is cancelled it is marked, and the mark takes effect at its next await that has to wait (see lang.await): that await throws a CancelledException. A task that never waits cannot be cancelled until it does.

CancelledException is an ordinary exception. It extends Exception, implements ICancelledException, and is caught like any other, so there is no second error channel:

interface ICancelledException : IException { }
class CancelledException : Exception, ICancelledException { }

A task may catch the exception, tidy up, and either rethrow it or carry on. If it carries on, it has refused cancellation, which is allowed and visible. An uncaught CancelledException in a task that belongs to a TaskGroup is absorbed at the group's boundary, because cancelling a task is not a program error. An uncaught exception of any other type is still an uncaught program error.

awaitTimeout makes a timeout an outcome instead of a failure:

T? awaitTimeout<T>(Promise<T> work, int ms)

It waits for work for at most ms milliseconds. It returns the value if work settles first, and None if the time runs out. It does not throw on timeout. Note what it does not do: it stops waiting, it does not cancel the work. A Worker is never cancelled by a timeout either, because a thread cannot be stopped by the clock. To combine the two, cancel the surrounding group yourself: int? r = awaitTimeout(p, 5000); if (r == None) g.cancelAll();.

awaitTimeout returns None on timeout

Promise<int> after(int ms, int value) {
    Promise<int> p = Promise();
    std::sysTimerStart(ms, 0, (n) => p.resolve(value));
    return p;
}

int? quick = awaitTimeout(after(10, 7), 1000);
console.writeln(quick ?? -1);

int? slow = awaitTimeout(after(500, 8), 20);
console.writeln("slow result is None: ${slow == None}");
7
slow result is None: true

Rules

  • Cancellation takes effect at the next await that actually waits, never earlier. Work that does not wait is never interrupted.
  • A cancelled await throws CancelledException with the message task cancelled.
  • A task may catch CancelledException and refuse to stop. It is then still running, and a group's close() keeps waiting for it.
  • awaitTimeout(work, ms) returns the value of work or None; it never throws on timeout. It can still rethrow a failure of work if work settles with one.
  • awaitTimeout never cancels work, and cannot stop a Worker.
  • While a task is running its own TaskGroup cleanup (the close() that a using runs), further cancellation is held back until that cleanup is over, so using inside a cancelled task cannot deadlock.
  • Cancellation is for tasks on the same thread. Workers started with spawn are separate threads and are not cancelled.

Examples

A timeout used as a kill switch for a group: the slow answer takes too long, so the group is told to stop its task. Leaving the using block waits for the task to finish cancelling:

Timeout, then cancel the group

Promise<int> after(int ms, int value) {
    Promise<int> p = Promise();
    std::sysTimerStart(ms, 0, (n) => p.resolve(value));
    return p;
}

void run() {
    using TaskGroup g = TaskGroup();
    g.run(() => {
        console.writeln("worker task: waiting for a slow answer");
        int v = await after(10000, 1);
        console.writeln("worker task: never reached");
    });
    Promise<int> slow = after(500, 2);
    int? answer = awaitTimeout(slow, 20);
    if (answer == None) {
        console.writeln("timed out, cancelling the group");
        g.cancelAll();
    }
}
run();
console.writeln("done");
worker task: waiting for a slow answer
timed out, cancelling the group
done

Notes

See std.TaskGroup for starting, cancelling and joining tasks as a group.

See also

  • TaskGroup — A set of tasks that live and end together.
  • CancelledException — The error delivered to a task that has been cancelled.
  • Promise — A value that arrives later.
  • Worker — The handle to a value that another worker is computing.