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:

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])
⚠️ Compiler Safety Guarantee
Because heap-backed data (strings, slices, arenas) cannot physically cross task boundaries, no two tasks can ever hold references to the same mutable memory. Data races are eliminated by construction.