thread
Native OS threads: pthreads on POSIX, the Win32 thread API on Windows.
| Item | Import |
|---|---|
spawn, try_spawn, JoinHandle, Builder, Scope | import std::thread; |
ThreadLocal<T> | import std::thread::local; |
Spawning
spawn(ctx, body) moves ctx into a new thread and hands it to body, a (C) -> T function — a named function or a non-capturing lambda. The returned JoinHandle<T> hands the body's result back through join().
import std::thread;
const h = thread::spawn<i32, i32>(21, (n: i32) -> i32 { return n * 2; });
const r: i32 = h.join(); // 42
Owned data — String, Box, Array — moves across the boundary in either direction: into the thread through ctx, and back out through the returned T. The move-checker tracks the transfer, so there is no aliasing and no double free.
The entry points require C: Send, T: Send, so moving a non-Send payload — an Rc, a lock guard — into another thread is a compile error.
Why an explicit context instead of a capturing closure? A capturing closure passed to a generic
(C) -> Tparameter does not compile yet. Passing the state explicitly throughctxis the design that works today; once the closure-capture carve-out lands, a capturingspawnbecomes sugar over this.
JoinHandle<T>
| Method | Notes |
|---|---|
join(mut this) | Waits and returns the body's T. |
detach(mut this) | Fire and forget. |
is_finished() | Polls without consuming the handle. |
id() | ThreadId |
A handle must be joined or detached. Dropping one without either is equivalent to detach().
try_spawn is the fallible variant, returning Result<JoinHandle<T>, SpawnError> instead of panicking when the OS refuses to create the thread.
Builder
Configure the stack size and OS thread name before spawning:
mut b: Builder = Builder::new().stack_size(8 * 1024 * 1024).name(Str::new("worker"));
const h = b.spawn<Job, Report>(job, run_job);
Builder::new(), stack_size(bytes), name(n: Str), then spawn or try_spawn.
Scoped threads
A Scope guarantees every thread spawned through it is joined before the scope is torn down — explicitly with join_all(), or automatically when the scope drops. That is what makes it safe for scoped bodies to borrow data outliving the scope: pass a pointer to it through the body's context.
mut counter: i64 = 0i64;
mut s: thread::Scope = thread::Scope::new();
s.spawn<i64*>(&counter, (p: i64*) -> void { *p = *p + 1i64; });
s.spawn<i64*>(&counter, (p: i64*) -> void { *p = *p + 10i64; });
s.join_all(); // both finished; `counter` is safe to read
Scope::new_with_stack(stack_bytes) gives each thread a larger stack, for bodies that recurse deeply where the roughly 1 MiB default would overflow. Scoped bodies return void, so there is no result handshake.
Cryo has no borrow checker, so the join-before-teardown discipline — not the type system — is what keeps borrowed data alive. Do not let a scope outlive what its threads point at.
Current thread
current() returns a Thread with an id(). ThreadId implements Eq.
yield_now() gives up the rest of the timeslice. sleep(nanos: u64) and sleep_ms(millis: u64) park the calling thread; for a Duration-typed API see time::sleep.
ThreadLocal<T>
Each thread that touches a ThreadLocal<T> lazily gets its own heap-allocated T on first access; later accesses from the same thread return the same slot. Threads never see each other's values.
import std::thread::local;
mut buf: ThreadLocal<String> = ThreadLocal<String>::new(() -> String { return String::new(); });
mut mine: String* = buf.get();
static new(init: () -> T), get() -> T*, and clear().
It is built on pthread_key_create with pthread_setspecific and pthread_getspecific; the per-thread value is heap-boxed and the void* TLS slot holds the box pointer. First access on a thread allocates and runs the init function pointer.
Cleanup is manual in v1.
pthread_key_createaccepts a destructor that runs at thread exit, and v1 passes null for it — so per-thread allocations are not freed automatically. Callclear()from each thread before it terminates, or accept the leak. For daemon-style threads that live as long as the process this is immaterial; short-lived workers should clear at exit. Wiring a per-Tmonomorphized destructor through the generic pipeline is what removes the limitation.
How a spawn works
Each spawn heap-allocates a payload (the body function pointer plus the moved-in context) and a control block (an atomic state byte plus storage for the result). The OS runs a monomorphized trampoline that moves the context out, runs the body, stores the result, frees the payload, and hands the control block off to join or detach through a lock-free two-actor handshake on the state byte. The payload is freed on every path, spawn failure included, so the control block never leaks.