Library
Tasks — suspension and interleaving
What a task is, what runs while one is suspended, and in what order suspended tasks resume.
since 0.1.0-alpha.1linuxwindows
Description
A task is the unit of suspension. You do not create tasks directly in most programs; they exist because of where code runs:
- the top level of the program is a task;
- every callback the event loop dispatches (a timer, a socket read, a worker join) runs as its own task;
TaskGroup.run(body)starts a task explicitly (seelang.cancellation).
When a task executes an await on a promise that is not ready yet (see lang.await), only
that task is suspended. The rest of the thread keeps going: the loop dispatches new callbacks as
new tasks, and other suspended tasks resume as their promises settle. This is what lets a
long wait in one place not freeze the rest of the program.
Two tasks waiting at once
Promise<int> after(int ms, int value) {
Promise<int> p = Promise();
std::sysTimerStart(ms, 0, (n) => p.resolve(value));
return p;
}
void worker(string name, int ms) {
console.writeln("${name}: waiting");
int v = await after(ms, ms);
console.writeln("${name}: resumed with ${v}");
}
std::sysTimerStart(0, 0, (n) => worker("slow", 40));
std::sysTimerStart(0, 0, (n) => worker("fast", 10));
console.writeln("main: top level done");
main: top level done
slow: waiting
fast: waiting
fast: resumed with 10
slow: resumed with 40
The top level finishes first, then the two callbacks run as two tasks. Both suspend, and each resumes when its own promise settles, so the shorter wait finishes first even though it was started second.
Rules
- Tasks stay on their thread. A task never moves to another thread, and
awaitnever moves work across threads.Worker<T>andChannel<T>are the only ways values cross threads (seelang.threads). - Resumption is first-come first-served. Of the tasks on one thread that are ready to continue, they run in the order they became ready. Do not rely on any other order, and in particular not on last-suspended-first-resumed.
- A ready
awaitis not a scheduling point. When the promise is already settled, the task continues immediately. A loop made only of such awaits is computation; other tasks get a turn only at the firstawaitthat really waits. - Interleaving is real. While a task is suspended, other tasks on the same thread run, and they
may read or change state the suspended task also uses. There are no data races (one thread, one
task at a time), but values read before an
awaitcan be out of date after it.awaitmarks exactly the places where this can happen. - Failures at an await. If a
Workerrejected, its failure is rethrown at theawait. If the loop has run out of work and the promise can never settle, theawaitthrows a catchableRuntimeException(await: event loop drained with promise unresolved). - An uncaught throw inside a callback ends the program. It is never delivered to an
unrelated
await; the waiting task later sees its own error. then(callback)runs synchronously in the context of the resolver; it does not create a task.awaitis a compile error insidecomptimecode.
Examples
Because other tasks run while one is suspended, a read-then-write around an await loses
updates. Both tasks read 0, wait, and both write 1; the counter ends at 1, not 2:
State can change across an await
class Counter { int n = 0; }
Counter counter = Counter();
Promise<int> after(int ms) {
Promise<int> p = Promise();
std::sysTimerStart(ms, 0, (n) => p.resolve(0));
return p;
}
void increment(string who) {
int seen = counter.n;
console.writeln("${who} read ${seen}");
int pause = await after(10);
counter.n = seen + 1;
console.writeln("${who} wrote ${counter.n}");
}
std::sysTimerStart(0, 0, (n) => increment("first"));
std::sysTimerStart(0, 0, (n) => increment("second"));
first read 0
second read 0
first wrote 1
second wrote 1
Several tasks waiting on one promise all resume, in the order they started waiting:
A gate that releases waiters in order
Promise<int> gate = Promise();
void waiter(string name) {
console.writeln("${name} waiting");
int v = await gate;
console.writeln("${name} released with ${v}");
}
std::sysTimerStart(0, 0, (n) => waiter("A"));
std::sysTimerStart(0, 0, (n) => waiter("B"));
std::sysTimerStart(0, 0, (n) => waiter("C"));
std::sysTimerStart(20, 0, (n) => {
console.writeln("opening the gate");
gate.resolve(1);
});
A waiting
B waiting
C waiting
opening the gate
A released with 1
B released with 1
C released with 1