Skip to content
CryoCryo home
StdlibConcurrency

thread

Native OS threads: pthreads on POSIX, the Win32 thread API on Windows.

public module thread::local;

Everything except ThreadLocal lives at the root of the module — import std::thread; brings it in.

FileWhat it holdsImport
localThreadLocal<T>, lazy per-thread storageimport std::thread::local;

Spawning

function spawn<C, T>(ctx: C, body: (C) -> T) -> JoinHandle<T> where C: Send, T: Send;

Spawn a thread that runs body(ctx) and returns a handle to its T result. ctx is moved into the new thread. body must be a function pointer or non-capturing lambda. Panics on allocation or pthread_create failure; use try_spawn for a fallible spawn.

function try_spawn<C, T>(ctx: C, body: (C) -> T) -> Result<JoinHandle<T>, SpawnError> where C: Send, T: Send;

Fallible spawn: like spawn, but returns Err(SpawnError) instead of panicking when the OS cannot create the thread (e.g. EAGAIN under a thread-count limit). The body's T is still delivered through the returned handle's join().

function spawn_with_attr<C, T>(ctx: C, body: (C) -> T, stack_bytes: u64) -> Result<JoinHandle<T>, SpawnError> where C: Send, T: Send;

Fallible spawn with an optional stack size (0 == OS default). The per-OS thread creator (os_create_thread) applies it - via a pthread_attr_t on POSIX, or CreateThread's dwStackSize on Windows.

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.

try_spawn is the fallible variant, returning Result<JoinHandle<T>, SpawnError> instead of panicking when the OS refuses to create the thread. spawn_with_attr additionally sets the stack size; Builder is the friendlier spelling of the same thing.

Why an explicit context instead of a capturing closure? A capturing closure passed to a generic (C) -> T parameter does not compile yet. Passing the state explicitly through ctx is the design that works today; once the closure-capture carve-out lands, a capturing spawn becomes sugar over this.

JoinHandle<T>

type struct JoinHandle<T> {
    tid:    u64;
    shared: Shared<T>*;

    join(mut this) -> T;
    detach(mut this) -> void;
    is_finished(&this) -> boolean;
    id(&this) -> ThreadId;
}
MethodNotes
join()Waits and returns the body's T. Consumes the handle.
detach()Fire and forget. Consumes the handle.
is_finished()Polls without consuming the handle.
id()ThreadId

A handle must be joined or detached. Dropping one without either is equivalent to detach().

SpawnError

type enum SpawnError {
    OutOfMemory;
    PermissionDenied;
    ResourceLimit;
    Other;
}

implement enum SpawnError {
    static from_errno(rc: i32) -> SpawnError;
}

Why the OS refused to create a thread, from try_spawn; from_errno is the mapping from the pthread return code.

Trait implementations

implement<T> trait Drop for struct JoinHandle<T>

Builder

type struct Builder {
    stack_bytes: u64;
    name_buf:    [u8;
    16];
    name_len:    u64;

    static new() -> Builder;
    stack_size(mut this, bytes: u64) -> Builder;
    name(mut this, n: Str) -> Builder;

    spawn<C, T>(this, ctx: C, body: (C) -> T) -> JoinHandle<T>
    where C: Send, T: Send;

    try_spawn<C, T>(this, ctx: C, body: (C) -> T) -> Result<JoinHandle<T>, SpawnError>
    where C: Send, T: Send;
}

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);

The name is capped at 15 bytes plus a terminator — the pthread_setname_np limit — and truncated past that. Each setter consumes and returns the builder, so the chain reads left to right; the terminal spawn consumes it too.

Scoped threads

type struct Scope {
    tids:        [u64];
    stack_bytes: u64;

    static new() -> Scope;
    static new_with_stack(stack_bytes: u64) -> Scope;

    spawn<C>(mut &this, ctx: C, body: (C) -> void) -> void
    where C: Send;

    join_all(mut &this) -> void;
}

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

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.

Trait implementations

implement trait Drop for struct Scope

Current thread

type struct Thread {
    id_: ThreadId;

    id(&this) -> ThreadId;
}
type struct ThreadId {
    raw: u64;

    equals(&this, other: &ThreadId) -> boolean;
}

current() returns a Thread with an id(). ThreadId compares through equals.

yield_now() gives up the rest of the timeslice. sleep(nanos) and sleep_ms(millis) park the calling thread; for a Duration-typed API see time::sleep. available_parallelism() is the number of hardware threads the process may use — sysconf(_SC_NPROCESSORS_ONLN) or the Win32 system info — and the natural size for a worker pool.

function current() -> Thread;

The thread invoking this call.

function yield_now() -> void;

Yield the remainder of the current timeslice to the scheduler.

function sleep(nanos: u64) -> void;

Sleep the current thread for at least nanos nanoseconds. On POSIX this restarts across signal interruptions (EINTR) so the full duration elapses; on Windows it rounds up to whole milliseconds.

function sleep_ms(millis: u64) -> void;

Sleep for at least millis milliseconds.

function available_parallelism() -> u32;

The number of hardware threads available to the process - the natural default size for a worker pool. Clamped to at least 1 so a pool always makes progress. Best-effort: returns 1 if the OS query fails.

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.

The sync primitives and Arc<T> back this surface.