OxigenLang Concurrency

OxigenLang runs parallel work on real OS threads across all your cores — not single-threaded async. The model is share-nothing: each task runs on its own VM with its own heap, and data crosses between threads by ownership transfer, not shared references. There are no locks, no mutexes, and no data races by construction.

The surface is two verbs — diverge to split work off the current flow, and converge to bring a result back.

#diverge / converge

#One task off to the side

diverge { ... } runs a block on a worker thread and immediately returns a task handle. Your code keeps going. converge waits for the task and gives you its value.

Oxigen
work := diverge { expensive() }   // starts running in the background
// ... do other things here while it runs ...
result := converge work           // wait for it, get the value

converge is idempotent — joining the same task twice returns the same value (it is remembered after the first join).

#Fan out over a collection

diverge each X in XS { ... } runs the block for every item in parallel and gathers the results into an array, in input order.

Oxigen
nums := [1, 2, 3, 4, 5, 6]

squares := diverge each n in nums {
    n * n
}
// squares == [1, 4, 9, 16, 25, 36]

Every iteration is in flight at once, so the wall-clock time is roughly one item's work, not the sum. This is the idiomatic way to parallelize a loop.

#Async API calls

Network calls in the net library are blocking — net.get(url) returns once the response arrives. You make them asynchronous by running them with diverge: a blocked worker stalls one thread, not your program. There is no async/await and no function coloring.

Oxigen
introduce net

urls := [
    "https://api.example.com/users/1",
    "https://api.example.com/users/2",
    "https://api.example.com/users/3",
]

// all three requests run at the same time
responses := diverge each url in urls {
    net.get(url)
}

each r in responses {
    println(r["status"])
}

Three sequential calls would take three round-trips; this takes one. The same pattern works for net.post, net.put, sockets, file I/O, or any blocking call.

#Timeouts

Add within <ms> to a converge to bound the wait. On timeout it returns an error value you can test with is_error; the task itself keeps running.

Oxigen
work := diverge { slow_job() }
result := converge work within 500   // wait at most 500ms

answer := option {
    is_error(result) -> "still working, gave up waiting",
    result
}

#Cancellation

cancel(task) asks a task to stop. Cancellation is cooperative — the task stops at its next function call or loop iteration, JIT-compiled loops included. Joining a cancelled task returns an error value (test it with is_error).

Oxigen
work := diverge { long_running() }
cancel(work)

result := converge work
done := option {
    is_error(result) -> "was cancelled",
    result
}

#Joining a batch

converge also accepts a list of tasks and joins them all, in order — no import needed. Use it to start several different tasks and collect them together:

Oxigen
user   := diverge { get_user(42) }
orders := diverge { get_orders() }

results := converge [user, orders]   // [userResult, ordersResult]

For fanning out the same work over a collection, use diverge each.

#Passing messages with channels

A task's result comes back once, when it finishes. To pass values while it runs, such as jobs for a worker to process in order, use a channel: a queue that any task can send to and recv from.

Oxigen
introduce channel

sentences := channel.new()

speaker := diverge {
    spoken := 0
    repeat when True {
        s := channel.recv(sentences)     // waits for the next one
        stop when s == None              // the channel was closed
        println("speaking: " + s)
        spoken = spoken + 1
    }
    spoken
}

each s in ["Hello.", "How are you?", "Goodbye."] {
    channel.send(sentences, s)
}
channel.close(sentences)
println(converge speaker)   // 3

Values are delivered in the order they were sent, and each is copied, like task arguments. A channel is an int handle, so it can be captured by a task or passed to one. channel.recv(ch, timeout_ms) stops waiting after a timeout, and channel.try_recv(ch) never waits. Signals reach a program the same way, on a channel from os.signals.

#What can cross threads

When a value is passed into a task or returned from one, it is copied across the thread boundary (the original side keeps its own copy). These types can cross:

  • Primitives: integers, floats, booleans, None
  • Strings
  • Arrays, tuples, maps, sets (including nested)
  • Structs and enums
  • Open TCP/UDP sockets (so a server can diverge each connection to a handler)
  • Channels, and processes started with os.spawn: like sockets, these are handles, so every task that has one uses the same channel or process

#Functions do not cross

A function is data everywhere else in Oxigen, but it is the one thing a task cannot carry across the boundary. The block you hand to diverge is not copied — the worker rebuilds it from the program source — so a value that happens to be a function has nothing to rebuild from.

Three ways to try it, and what each does:

Oxigen
fun handler(x) { x * 2 }

// 1. Captured by the block — raises immediately.
t := diverge { handler_local(3) }
// spawn(): capture cannot send value of type FUNCTION across threads

// 2. Passed as an argument — raises immediately.
t := __spawn(run_it, handler)
// spawn(): cannot send value of type FUNCTION across threads

// 3. Inside a map or array the block reads — the quiet one.
table := {"a": handler}
t := diverge { keys(table) }
converge t     // error: undefined variable: table

The third is the one to watch for. A collection holding a function is dropped from the snapshot the worker receives rather than refused, so the failure surfaces later as an undefined variable naming a variable you can plainly see defined above.

What to do instead: send names, not functions. A worker VM registers every top-level declaration in your program, so top-level functions — including name := fun(x) { ... } bindings — already exist on it. Build the table that holds them inside the task:

Oxigen
handler_a := fun(req) { "A" }
handler_b := fun(req) { "B" }

fun dispatch(which, req) {
    table := {"a": handler_a, "b": handler_b}   // built on the worker
    table[which](req)
}

results := diverge each name in ["a", "b"] {
    dispatch(name, "req")                       // only strings cross
}

This is why api has you register routes inside a function that each worker calls, instead of configuring one app and handing it over.

#Limitations

The model is built for parallel compute and I/O fan-out into the hundreds. A few current edges to know:

  • One OS thread per in-flight task. Excellent to a few hundred concurrent tasks; there is no event-loop reactor yet, so this is not tuned for tens of thousands of idle sockets.
  • Workers always use the default tiered JIT. They ignore the main process's --jit / --no-jit flags; heavy per-task loops still JIT on their own.
  • Auto-join only triggers on + today. A task used in a + b resolves automatically, but other operators (*, ==), indexing, and println still need an explicit converge.
  • Structured converge { ... } blocks are not implemented yet — use converge <task> or diverge each.
  • Keep tasks as leaves. A task's own diverge runs inline on its worker, and tasks may nest at most 64 deep; past that the innermost diverge returns an error value. Have a task do work and return, rather than orchestrate more tasks.
  • Each task imports the modules it uses afresh. A task gets its own copy of every module it reaches, with fresh module state, so a module's top level runs again for each task that uses it. For thousands of tiny tasks that each touch a large module, give each task a batch of items instead.

#Quick reference

FormMeaning
diverge { B }run B on a worker, return a task
diverge each X in XS { B }run B for each item in parallel, gather results
converge Twait for task T, return its value
converge [T1, T2, …]wait for a list of tasks, return their values in order
converge T within mswait up to ms; error value on timeout (test with is_error)
cancel(T)ask task T to stop (cooperative)
channel.send(C, V) / channel.recv(C)pass V to whichever task receives from channel C