Async Programming
Overview
Section titled “Overview”Ard’s concurrency model is Go’s: lightweight goroutines that communicate over
typed channels. There are no async/await keywords and no promises. Instead,
two small primitives compose into everything else:
ard/asyncstarts goroutines.- Built-in channels (
Chan<T>,Receiver<T>, andSender<T>) pass typed values between them.
Waiting for completion, returning results, joining a set of tasks, fan-in, and timeouts are all written in ordinary Ard on top of these — not baked into the language.
Starting goroutines
Section titled “Starting goroutines”async::start runs a function on a new goroutine. It is fire-and-forget: it
returns immediately and gives you no handle.
use ard/asyncuse go:fmt
fn main() { let done = Chan::new<Bool>() async::start(fn() { fmt::Println("running concurrently") done.send(true) }) done.recv()}Because start returns nothing, you coordinate with a channel rather than a
return value. Spawned closures follow Go’s semantics: they capture by reference,
and there is no isolation rule — shared state is coordinated through channels and
data races are your responsibility.
Coordinating with channels
Section titled “Coordinating with channels”A channel is a typed conduit between goroutines. send blocks until a value is
taken (or buffered); recv blocks until a value arrives or the channel is
closed.
Waiting for completion
Section titled “Waiting for completion”use ard/asyncuse go:fmt
fn main() { let done = Chan::new<Bool>()
async::start(fn() { fmt::Println("working") done.send(true) })
done.recv() // blocks until the goroutine signals fmt::Println("finished")}Returning a result
Section titled “Returning a result”A goroutine “returns” by sending its result on a channel. This is the pattern that replaces a result-returning task: start the work, then receive its value.
use ard/async
fn compute() Int { // ... expensive work ... 42}
fn main() Int { let result = Chan::new<Int>()
async::start(fn() { result.send(compute()) })
result.recv().or(0)}Joining many goroutines (fan-in)
Section titled “Joining many goroutines (fan-in)”To wait on several goroutines, have each send to the same channel and receive once per goroutine:
use ard/asyncuse ard/list as List
fn main() Int { let inputs = [1, 2, 3, 4] let results = Chan::new<Int>(inputs.size())
for n in inputs { async::start(fn() { results.send(n * n) }) }
mut total = 0 for _ in inputs { total = total + results.recv().or(0) } total // 30}Closing and draining
Section titled “Closing and draining”The sending side closes a channel when it is done producing. A receiver loops
until recv() returns none, the closed-and-drained signal:
use ard/async
fn main() Int { let jobs = Chan::new<Int>()
async::start(fn() { jobs.send(1) jobs.send(2) jobs.send(3) jobs.close() })
mut total = 0 mut draining = true while draining { match jobs.recv() { n => { total = total + n }, _ => { draining = false }, } } total // 6}Directional channels
Section titled “Directional channels”An API can express direction by narrowing a bidirectional Chan<$T> to a
receive-only Receiver<$T> or a send-only Sender<$T>. Narrowing is explicit
and one-way:
fn produce(out: Sender<Int>) { out.send(1) out.close()}
fn main() { let ch = Chan::new<Int>() produce(ch.sender()) // ch.recv() ...}Waiting on many channels with select
Section titled “Waiting on many channels with select”select waits on several channel operations at once and runs the arm whose
operation is ready first. Unlike a sequence of recv calls, it registers every
arm together and blocks until one can proceed. If several are ready, one is
chosen at random (Go’s fairness). It is an expression, so it can produce a value.
select { let job = jobs.recv() => match job { j => run(j), // received a value _ => drain(), // jobs closed (recv() == none) }, results.send(value) => recorded(), // a send arm timeout.recv() => give_up(), // receive and discard _ => idle(), // non-blocking default}let name = ch.recv() => bodybindsname: $T?; a closed channel surfaces asnone, handled with a normalmatch.ch.recv() => bodyreceives and discards the value.ch.send(x) => bodysends an in-scope value; with a_arm it is a non-blocking try-send._ => bodyruns when no other arm is ready, making the wholeselectnon-blocking.
Timeouts
Section titled “Timeouts”Go’s time::After returns a channel that fires after a delay, which composes
naturally as a select arm:
use go:time
fn main() Int { let work = Chan::new<Int>() // ... a goroutine may eventually send on `work` ...
select { let v = work.recv() => v.or(0), time::After(time::Second).recv() => -1, // timed out }}Design philosophy
Section titled “Design philosophy”Ard deliberately keeps async tiny. The only thing the language must provide is a
way to start a goroutine, because there is no go statement to write in
userland. Everything a richer async library would offer — futures, joins,
wait-groups, structured concurrency — is expressible as plain Ard over channels,
so it lives in libraries and programs rather than in the runtime. The runtime
itself defines no async type; start is a compiler-backed intrinsic, and channels lower
to native chan T.