Shared-Nothing Concurrency & Tasks
Inspired by Go's goroutines and channels, Ube provides shared-nothing concurrency with tasks (spawn), typed channels (Chan[T]), and non-blocking event multiplexing (select)—enforcing zero data races statically.
1. Shared-Nothing Philosophy
Traditional multithreading relies on shared mutable memory protected by mutexes, condition variables, and locks. This model is notoriously prone to deadlocks, race conditions, and cache thrashing.
In Ube, tasks communicate exclusively by passing messages through typed channels. Tasks do not share heap memory or mutable pointer structures.
2. Spawning Tasks with spawn
To launch a concurrent fiber task, prepend spawn to a function call. The target must be a direct top-level function call:
fn background_counter(limit: i32) {
for i in 1..limit + 1 {
println(f"Counter tick #{i}")
}
}
fn main() {
spawn background_counter(5)
println("Main fiber initiated counter task.")
}
3. Typed Channels (Chan[T])
Channels are typed, buffered queues created with Chan[T].new(capacity). Channels provide three primary operations:
ch.send(val): Appends a value into the channel buffer.ch.recv(): Receives the next value, returningT?(returnsnullwhen the channel is closed and empty).ch.close(): Marks the channel closed so receivers know no further data will arrive.
struct WorkItem {
id: i32,
priority: i32
}
fn processor(inbox: Chan[WorkItem], outbox: Chan[i32]) {
while let item = inbox.recv() {
println(f"Processing WorkItem #{item.id} with priority {item.priority}")
outbox.send(item.id * 10)
}
outbox.close()
}
fn main() {
let inbox = Chan[WorkItem].new(8)
let outbox = Chan[i32].new(8)
spawn processor(inbox, outbox)
inbox.send(WorkItem { id: 1, priority: 100 })
inbox.send(WorkItem { id: 2, priority: 50 })
inbox.close()
while let result = outbox.recv() {
println(f"Completed job ID: {result}")
}
}
4. Event Multiplexing with select
The select statement lets a task wait on multiple channel receive operations simultaneously. An optional default arm enables non-blocking checks:
fn main() {
let ch_fast = Chan[i32].new(2)
let ch_slow = Chan[i32].new(2)
ch_fast.send(42)
select {
val = ch_fast => {
println(f"Received from fast channel: {val}")
},
val = ch_slow => {
println(f"Received from slow channel: {val}")
},
default => {
println("No channel ready immediately.")
}
}
}
5. The Flat Type Invariant (CRITICAL)
How does Ube guarantee zero data races without locking overhead? Through the Flat Type Invariant.
Values sent through a channel or passed as arguments to spawn must be flat:
| Flat Types (Allowed Across Tasks) | Non-Flat Types (REJECTED by Compiler) |
|---|---|
Primitives (i32, f32, u8, bool) |
string (heap/arena allocated byte buffer) |
Fixed-size arrays of flat types ([N]i32) |
Dynamic slices ([]T) |
| Structs and enums containing only flat types | StringBuilder |
Channels of flat types (Chan[T]) |
Generational Entity Pools (Pool[T]) |