Tasks and channels
Programs can run many tasks at once, like goroutines, which talk to each other through channels: on the VM’s scheduler in hosted programs, and as threads on every core in native ones.
enum Msg { Item(i64), Done }
fn numbers(out: chan Msg, n: i64) { let i = 0 while i < n { send(out, Msg.Item(i)) i = i + 1 } send(out, Msg.Done)}
fn main() { let c = chan(Msg) // unbuffered; `chan(Msg, 8)` buffers 8 values go numbers(c, 5) let total = 0 let running = true while running { match recv(c) { Item(x) => total = total + x Done => running = false } } print(total) // 10}go f(x) runs a call in a new task. The function and its arguments are
evaluated first, in the task that says go; any function value works, closures
included:
go worker(jobs, results)go fn() { send(results, work(id)) }()The result of the call is dropped; send it over a channel to get it back.
Channels
Section titled “Channels”chan(T)makes an unbuffered channel ofTvalues, andchan(T, n)one that buffers up ton. The type ischan T.send(c, v)waits while the channel is full; an unbuffered one is full until a receiver comes, so sender and receiver meet.recv(c)waits until there is a value, and returns it.- There is no
close. To say “no more values”, send a value that says so, asMsg.Doneabove;matchthen handles both cases.
Tasks share nothing but channels: arguments are copies, closures capture by value, and refs are immutable. So there are no data races to worry about. (Hosted tasks may also share cells; native ones, the default, may not.)
select
Section titled “select”select waits on several channels at once (examples/select.jh):
select { let n = recv(numbers) => total = total + n recv(quit) => running = false send(log, line) => {} _ => print("nothing ready") // optional: do not wait}- The cases are
recv(c),let x = recv(c)(binding the value for the arm),send(c, v), and an optional last_. - The channels and the values to send are evaluated first, in order.
- Then the first case that can go ahead without waiting does, and its arm runs. If several can, the first in source order wins (Go picks at random; jihoo stays reproducible).
- With
_and no case ready, the_arm runs. Otherwise the task waits on all the channels, and the first case another task makes possible goes ahead; its waits on the other channels are cancelled.
Scheduling
Section titled “Scheduling”In a hosted program (#![hosted]), tasks run on one OS thread. The VM’s
scheduler is round-robin and
deterministic: a task runs until it waits on a channel, finishes, or has run
1000 instructions and reaches a safepoint (a call, or a loop going round);
then the next ready task gets its turn.
- The same program prints the same output on every run, which keeps tests reliable.
- Code without calls and loops is never interrupted, which is what makes
updates of cells such as
*n = *n + 1safe.
Ending
Section titled “Ending”- The program ends when
mainreturns, even if other tasks are still running or waiting (as in Go). - If every task waits on a channel, the run stops with
deadlock: every task is waiting on a channel. - An error in any task stops the whole run.
In native programs
Section titled “In native programs”Natively go f(x) starts a thread, and chan, send,
recv and select work as above, on the channel library, which the
compiler adds to a native program that uses them. The differences:
- Tasks run at the same time, on every core, so their order is not deterministic: a program whose output must not depend on it waits for what it prints, through channels, as a correct Go program does.
- Tasks share no cells: a cell in the arguments
of
go, or in the values of a channel, is an error, and starting a closure that captured one stops the program. Share state by sending values instead. - A deadlock is not detected: the waiting threads wait forever.
- A
selecthas at most 16 cases.
A value sent moves into the channel and then to the receiver, so sending a list copies nothing; values still in a channel when its last copy goes are freed with it. Freestanding programs have no tasks or channels, and native programs also have coroutines.
In JIR, tasks and channels are the chan, send, recv, select and
spawn instructions.