Concurrency
Tasks, channels, select, and cancellation — the current bootstrap concurrency baseline.
Concurrency is one of the reasons gof exists, so it should be learned carefully.
The current implementation already exposes a real baseline, but it is still a bootstrap baseline. That means you can write concurrent examples today, but you should not confuse that with a finished production runtime contract.
Step 1: go and await
fn square(x: int) -> int:
return x * x
fn main() -> int:
job: task[int] = go square(12)
return await jobCurrent rules:
gocurrently spawns a top-level named function call- the result is a task value
awaitwaits for that task valueawait_result(task)joins any task asResult[T, RuntimeError]without changing the existing plain-awaitcontractawait_result(task, token)lets the join-site returnResult.Err(RuntimeError.Cancelled)if that token is cancelled before the task completes- when the spawned function explicitly declares
-> Result[..., RuntimeError], task-boundary evaluator failures now come back asResult.Err(RuntimeError.TaskFailed(...))orResult.Err(RuntimeError.TaskPanicked(...))instead of tearing down the caller immediately
If you want recoverable joins for a plain task[T] today, use await_result(task):
fn lucky() -> int:
return 7
fn main() -> Result[int, RuntimeError]:
job: task[int] = go lucky()
return await_result(job)fn slow() -> int:
sleep(25)
return 7
fn main() -> Result[int, RuntimeError]:
job: task[int] = go slow()
return await_result(job, timeout_token(0))The mental model is:
gocreates concurrent workawaitjoins that work back into the current flow
Step 2: channels and explicit lifecycle
fn main() -> Result[int, RuntimeError]:
ch: channel[int] = channel()
send(ch, 4)?
return recv(ch)Current channel baseline:
channel[T]is a real parameterized builtin type annotationchannel()creates a bootstrap channelchannel(0)creates a rendezvous channel baselinechannel(n)forn > 0creates a bounded channel baselineclose(channel)closes it explicitlysend(channel, value)returnsResult[unit, RuntimeError]recv(channel)returnsResult[T, RuntimeError]
With channel lifecycle, the bootstrap runtime already distinguishes:
- successful send/receive
- closed channel
- cancelled wait
Current capacity rules:
channel()keeps the existing unbounded queue-backed bootstrap pathchannel(0)blockssend(...)until a receiver takes the valuechannel(n)withn > 0blockssend(...)when the buffer is full until a receiver frees space
Step 3: select
fn main() -> Result[int, RuntimeError]:
left: channel[int] = channel()
right: channel[int] = channel()
send(right, 8)?
select:
value = recv(left):
return Result.Ok(value? + 0)
value = recv(right):
return Result.Ok(value? + 1)Current select contract:
- receive arms, send arms, and a single
defaultarm are supported - receive arms must be
recv(channel):,value = recv(channel):,recv(channel, token):, orvalue = recv(channel, token): - send arms must be
send(channel, value):,value = send(channel, value):,send(channel, value, token):, orvalue = send(channel, value, token): default:executes immediately when no send/receive arm is ready during the current polling pass- the bootstrap runtime prepares each send/receive operation once at select-entry, then blocks on channel/token wakeups between polling passes until one resolves to
Result.Ok(...)orResult.Err(...) - when multiple send/receive arms are already ready, the bootstrap runtime rotates the ready-arm start index in a deterministic round-robin baseline so the first source arm does not always win
Step 4: cancellation
fn main() -> bool:
token = timeout_token(0)
manual = cancel_token()
cancel_after(manual, 0)
return is_cancelled(token) and is_cancelled(manual)Current cancellation baseline:
cancel_token()creates a cooperative tokencancel(token)marks the token as cancelledis_cancelled(token)reports the current statetimeout_token(milliseconds)creates a token that cancels itself after a non-negative delaycancel_after(token, milliseconds)schedules cancellation for an existing token after a non-negative delaysend(..., token),recv(..., token), andawait_result(task, token)wake promptly when that token flips instead of waiting for fixed timeout polling ticks
What is still missing
This is a real concurrency baseline, but not the final story.
Still missing:
- production-grade fairness guarantees beyond the current round-robin ready-arm baseline
- automatic task panic and boundary-failure preservation for plain
await taskjoins without opting intoawait_result(task) - deadline/context propagation beyond timeout-backed token baselines
- production scheduler hardening
The right way to read current concurrency docs
Think of current gof concurrency as:
- real enough to learn from
- real enough to test
- not yet strong enough to promise production-grade semantics
That distinction matters because concurrency is one of the easiest places for a language project to oversell itself.