Go-Style Concurrency

Goroutines, and channels that move values between threads in order.

Threads can also talk to each other the way Go does. go(f, ...args) starts a goroutine, and a Channel<T> moves values between threads in order. You can send and recv, loop with for (const v of channel), and wait on several channels with select.

When you send a struct or a SharedArray on a channel, it stays where it is, in shared memory. The receiver reads the same one.

Closed Channels

recv() returns undefined both for a sent undefined and once the channel is closed. To tell the two apart, use const [v, ok] = channel.recvOk(), just like Go's v, ok := <-ch. select gives you the same ok, as its third element. So a Channel<void> either signals by close(), which wakes every receiver, or is read with recvOk().

Async Goroutines

A goroutine of an async function is light, just like Go's are. One worker can run thousands of them, and each one waits with await without blocking the others. Here's what each kind costs in Node on an M5 Pro:

Goroutine Time to start Memory
On a worker of its own 3.2 ms 8.6 MB
Of an async function 0.2 µs About 350 bytes

A blocking call in an async function would stop every goroutine on its worker. So the checker reports it, and the runtime throws.

interface Replica {
  position: i32;
  changed: Channel<void> | null;
} // Closed, and replaced, on every change.
const Replica = struct<Replica>({position: i32, changed: ref<Channel<void>>()});

async function request(
  replica: Replica,
  position: i32,
  answers: Channel<string>,
) {
  for (;;) {
    const changed = replica.changed!;
    if (atomic.load(replica, 'position') >= position) break;
    await changed.recvAsync(); // One of thousands waiting on this worker.
  }
  await answers.sendAsync('ready');
}

for (let k = 0; k < 10_000; k++) go(request, replica, (k % 1000) + 1, answers); // 0.2 µs each

Goroutines and Channels in the API reference lists every call.

Docs