Concurrency
Overlap waiting work with coroutines and coordinate shared state.
On this page
Coroutines let functions make progress while other functions wait. Threads let work run on separate CPU threads. Choose between them based on whether the work mainly waits for I/O or needs CPU time.
Start work and await its result#
use valk.time
fn delayed_value(value: int) int {
time.sleep_ms(10)
return value
}
fn main() {
let first = co delayed_value(10)
let second = co delayed_value(20)
let a = await first
let b = await second
println(a + b) // 30
}
co creates a coroutine call. await waits for completion and retrieves the result. Both calls are started before either result is awaited, so one can make progress while the other is suspended.
A coroutine does not automatically put a CPU-heavy calculation on another thread. Use the thread API for CPU parallelism.
Handle failure at await#
When a coroutine returns an error, handle it on the await:
fn parse_count(text: String) int !SyntaxError {
return text.to(int) !>
}
fn main() {
let pending = co parse_count("42")
let count = await pending !? 0
println(count)
}
Await work whose result or failure matters. A coroutine that is never awaited does not report its error to a caller. Coroutine error types cannot contain payload fields; handle those errors inside the coroutine or represent the outcome in a return value.
Run work on a thread#
use valk.thread
fn main() {
let worker = thread.Thread[int].start(fn() int {
return 6 * 7
}) ! panic("Could not start a worker")
println(worker.await()) // 42
}
Thread[int] declares the worker's result type. Starting a thread can fail, and worker.await() joins it and returns its result. The example uses no shared mutable input; when workers need common state, make that sharing explicit.
Protect shared state#
Use Lock[T] for an object that multiple threads need to modify. Access its value only inside a lock block:
class Progress {
completed: int (0)
}
fn main() {
let progress: shared Lock[Progress] = .new(Progress{}) !!
lock progress as state {
state.completed++
println(state.completed)
}
}
The block holds the lock until it exits. state is a temporary, mutable locked Progress view; it cannot be saved or returned for use after the block. Values reached through it carry the same restriction. Make an independent copy inside the block if data must leave it.
Creating a shared view publishes the object graph for cross-thread use. The compiler requires that graph to be unique and prevents ordinary aliases from continuing to use the published data. Prefer constructing the object directly where you publish it, as above.
Keep lock blocks short. A lock is not reentrant: trying to acquire the same lock again while holding it can deadlock. For compound updates, keep the related reads and writes in the same block.
Use an atomic operation for a number#
atomic(counter + 1) updates a numeric location atomically and returns its previous value. This can suit a counter, but does not make a larger sequence of reads and writes atomic. Use a lock when several fields must stay consistent together.