# threads.thread

> A module of threads's API.

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`

```mojo
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`

```mojo
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`

```mojo
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`

```mojo
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`

```mojo
def is_joinable(self) -> Bool
```

Whether this handle still names a thread.

#### `pin_to_cpu`

```mojo
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`

```mojo
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`

```mojo
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`

```mojo
def current_thread_id() -> UInt64
```

The calling thread's `pthread_self()`.

### `yield_now`

```mojo
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

### `StartFn`

```mojo
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.
