LEVIATHAN v962456e · 962456eee1

Library

The event loop and program lifetime

When a program is finished, what keeps it alive after the last statement, and how callbacks are dispatched.

since 0.1.0-alpha.1linuxwindows

Description

Every program has an event loop. The statements at the top of your file run first, in order. When they finish, the loop takes over: it waits for timers, sockets and other sources of events, and runs your callbacks as they become ready. You never start the loop; it is implicit.

The program stays alive for as long as live work remains, and it exits on its own when there is none:

  • a pending timer is live work; a one-shot timer releases itself after it fires, and cancel() releases a repeating one;
  • an open socket with a read watch, a listener that is accepting, a subscription to a signal and a running child whose exit is being waited for are live work too;
  • a program with no live work exits as soon as its top-level statements are done.

Callbacks run on a single thread, one at a time. Two callbacks never run at the same instant, so they cannot race on the program's data. When more than one timer is due, they fire in due-time order, and for equal due times in the order the timers were created.

A repeating timer keeps the program alive until it is cancelled

Timer once = std::after(100);
once.subscribe((n) => console.writeln("one-shot fired, tick ${n}"));

Timer repeat = std::every(10);
repeat.subscribe((n) => {
    console.writeln("repeat ${n}");
    if (n == 3) { repeat.cancel(); }
});

console.writeln("top level done");
top level done
repeat 1
repeat 2
repeat 3
one-shot fired, tick 1

The top-level statements finish first, so top level done is printed before any callback runs.

Rules

  • The loop starts after the top-level statements finish; there is nothing to call.
  • The program exits when no live work remains. A repeating timer that is never cancelled keeps it running forever.
  • Dispatch is single-threaded: callbacks never run concurrently. Worker threads started with std::spawn have their own loop; see lang.threads.
  • Timers fire in (due time, creation order).
  • An uncaught exception in a callback stops the loop and reports the error as usual, ending the program with status 1.
  • To end the program early, call env::exit; see lang.exit-codes.

Examples

Timers fire in due-time order, whatever order they were created in:

Dispatch order of timers

Timer a = std::after(30);
Timer b = std::after(10);
Timer c = std::after(20);
a.subscribe((n) => console.writeln("a (30ms)"));
b.subscribe((n) => console.writeln("b (10ms)"));
c.subscribe((n) => console.writeln("c (20ms)"));
Timer never = std::every(5);
never.cancel();
b (10ms)
c (20ms)
a (30ms)

The timer never was cancelled before the loop started, so it does not keep the program alive and never fires.

Notes

The loop is the same one that drives sockets, signals, child processes and timers, so a program that mixes them needs nothing more than callbacks and await.

See also

  • Timer — A source of ticks on the event loop.
  • Promise — A value that arrives later.
  • TaskGroup — A set of tasks that live and end together.