threads · API

threads.thread

OS threads: ThreadHandle, num_cpus, current_thread_id, CPU pinning.

A thin, honest wrapper over pthread_create / pthread_join. There is no thread pool here and no runtime — one ThreadHandle is one kernel thread, and you are responsible for joining it.

The send contract

pthread_create takes a C function pointer and a single void *. That is the entire channel, and this library does not pretend otherwise:

  • the start routine must be a thin (non-capturing) function def(OpaquePtr) thin -> OpaquePtr;
  • everything the thread needs travels through the OpaquePtr argument;
  • the caller guarantees that whatever the pointer addresses outlives the join.

That last point is not a style preference. Mojo destroys a value at its last use, not at the end of the enclosing scope, so a context that the main thread stops mentioning after spawn can be freed while the worker is still reading it. Keep the context alive across the join — mention it after join(), or heap allocate it and free it after join(). threads.parallel.parallel_for does the latter, and tests/test_threads.mojo has a test that fails loudly if the ordering is ever broken.

Platforms

pthread symbols are resolved with plain external_call, no dlopen. On macOS pthread lives in libSystem, which is always linked; on Linux with glibc 2.34 or newer libpthread is folded into libc. Both are in the default dynamic link namespace, so the symbols resolve without any link flag. This is verified on ubuntu-latest and macos-latest in CI.

Structs

ThreadHandle

struct ThreadHandle

An owning handle to one live OS thread.

Movable but deliberately not Copyable. A pthread_t names one kernel thread and POSIX forbids joining the same one twice, so "exactly one owner, exactly one join" belongs in the type system rather than in a comment.

As defence in depth, join() also zeroes the stored id, so a second join() on the same handle short-circuits instead of calling pthread_join on a stale id (which is undefined behaviour, not an error return).

Implements Movable

spawn

def spawn[start: def(Pointer[UInt8, MutUntrackedOrigin]) thin -> OpaquePtr](arg: Pointer[UInt8, MutUntrackedOrigin]) -> Self

Start a thread running start(arg).

Parameters
start: The thread body. Thin (non-capturing) and non-raising — pthread has no exception channel, so convert any failure into a value the caller can read out of arg.
Args
arg: The context pointer handed to start. Must stay valid until the thread is joined.

Raises Error: If pthread_create fails, carrying its POSIX return code (EAGAIN when the process is out of threads, EPERM for scheduling-attribute refusals).

join

def join(mut self)

Wait for the thread to finish; discard its return value.

Idempotent: after a successful join the handle's id is zeroed, so calling join() again returns immediately instead of re-entering pthread_join with a stale id.

Raises Error: If pthread_join fails. The handle is left untouched so the caller can retry or propagate.

detach

def detach(mut self)

Release the thread without waiting for it; it cleans itself up when it returns.

After this the handle is inert — join() becomes a no-op. Only detach a thread whose context outlives the whole process, or one that owns its context outright: nothing is left to tell you when it stopped reading.

Raises Error: If pthread_detach fails.

is_joinable

def is_joinable(self) -> Bool

Whether this handle still names a thread.

pin_to_cpu

def pin_to_cpu(self, cpu: Int)

Pin the thread to one logical CPU.

On Linux this is a real, hard affinity set via pthread_setaffinity_np. On macOS it is a documented no-op: Darwin exposes only thread_policy_set(THREAD_AFFINITY_POLICY), which is an affinity hint used to group threads onto a shared cache, not a pin, and pretending otherwise would be worse than doing nothing.

Args
cpu: Zero-based logical CPU index.

Raises Error: On Linux if pthread_setaffinity_np fails — typically EINVAL for a cpu index outside the process's cpuset, which is what happens inside a restricted container. Never raises on macOS.

Functions

pin_current_to_cpu

def pin_current_to_cpu(cpu: Int)

Pin the calling thread to one logical CPU.

ThreadHandle.pin_to_cpu pins a thread you spawned; this pins the one you are on. That is the more common shape in a worker pool, where each worker picks its own core after starting rather than having the spawner reach in.

A free function rather than a ThreadHandle built from pthread_self deliberately: such a handle would look joinable, and joining it means a thread joining itself. There is no safe handle for "me", only operations.

On Linux a real affinity set; on macOS a documented no-op, for the reason given on ThreadHandle.pin_to_cpu.

Args
cpu: Zero-based logical CPU index.

Raises Error: On Linux if pthread_setaffinity_np fails — typically EINVAL for a cpu outside the process's cpuset, as in a restricted container. Never raises on macOS.

num_cpus

def num_cpus() -> Int

The number of logical CPUs available to this process.

Uses sysconf(_SC_NPROCESSORS_ONLN), whose constant differs between the platforms (84 on Linux, 58 on Darwin).

current_thread_id

def current_thread_id() -> UInt64

The calling thread's pthread_self().

yield_now

def yield_now()

Hand the rest of this time slice to another runnable thread (sched_yield). The polite thing to do inside a spin loop.

Aliases

comptime StartFn = def(Pointer[UInt8, MutUntrackedOrigin]) thin -> OpaquePtr

The one callable shape that can cross a thread boundary: a non-capturing function taking and returning the opaque context pointer.