Processes and threads

Processes and threads

import std::process;
import std::threads;

Spawning a process

[public] fn spawn(program: string, args: Array<string>) => Process?
[public] fn spawn_in(program: string, args: Array<string>, cwd: string) => Process?
[public] fn run(program: string, args: Array<string>) => Output?

run waits and returns Output { exit_code, stdout, stderr }, draining stderr on its own thread so a full pipe cannot deadlock. spawn gives you the running child:

[public] fn pid() => number
[public] fn write(text: string) => number        // negative on error
[public] fn write_line(text: string) => number   // negative on error
[public] fn close_input()
[public] fn read_line() => string?
[public] fn read_error_line() => string?
[public] fn read(count: number) => string?
[public] fn on_output(callback: closure(string) => void)
[public] fn on_error(callback: closure(string) => void)
[public] fn wait() => number
[public] fn is_running() => bool
[public] fn try_exit_code() => number?
[public] fn kill()
[public] fn free()

read_line, read, and wait block. on_output and on_error spawn a background thread and return immediately, which is how you stream a long-running child. read(count) reads a fixed byte count for framed protocols.

free() closes the pipes but does not stop the child: kill() first if it is still running.

Threads

[public] fn sleep(seconds: number)
[public] fn sleep_ms(ms: number)
[public] fn new_thread(work: closure() => void)
[public] fn make_future<T>(work: closure() => T) => Future<T>
[public] fn scope(body: closure(Scope) => void)

new_thread is the workhorse. Capture what the thread needs:

threads::new_thread(closure[application, child]() => void {
    let code: number = child.wait()
    application.emit("done", `{"code":${code}}`)
})

GC participation is automatic: the runtime attaches and detaches each worker, and every blocking call in the standard library is bracketed so a collection can run while a thread waits.

Mutex

let guarded: threads::Mutex<Map<string, number>> =
    new threads::Mutex<Map<string, number>>(new Map<string, number>())

let map: Map<string, number> = guarded.lock()
let present: bool = map.contains(key)
guarded.release()

A mutex is not scoped. lock() returns the guarded value and release() has to run on every path out, including early returns. There is no guard object and no try_lock. Hold it across the read, then release before doing real work.

Channels, futures, cancellation

Channel<T> with a buffer size of zero is a rendezvous; a positive size queues. recv() returns None when the channel is closed and drained, while try_recv() returns None for empty or closed, which are not distinguishable.

Future<T> from make_future has await(), await_timeout(ms), is_complete(), and cancel(). await returns None when cancelled.

CancelToken is the safe way to stop work: check is_cancelled() in the loop. Thread.kill() exists but may leave a lock held or an allocation half-finished.

Mutex, Channel, Future, CancelToken, and Rng each own memory outside the managed heap and must be released with free() when done.