hosted/thread
skip to the library navigationHosted targets only.
Threads: tasks a routine starts as a group and waits for before it goes on, each on a thread of its own, written once over platform/posix.
| Call | What it does |
|---|---|
thread.group_over(host, backing, count) | Cut a backing the caller lends into count equal stacks, over a hosted/system handle, and return the group from both. |
thread.admit | Wait for a free stack, copy a state onto it and start a thread that calls a routine with the copy. |
thread.join | Wait until every thread has ended, and fail with the first failure any task had. |
thread.least_stack() | The least bytes one stack takes on this system. |
It is core/fibre's group over threads: a task is an ordinary routine, (inout state: T) -> none ! failure.reason, its state is copied onto its own stack and kept escaping into the group, and a routine making a group defers thread.join(group) else none as it would a region's release. Threads are no provider of core/fibre's concepts: a thread runs beside the one that started it, so what they share between admit and join is the atomic wrapper's or a lock's.
- Everything the routine did before
admithappens before the task's first action, and everything the task did happens beforejoinreturns. Nothing else orders the two. joinblocks the calling thread, so a fibre that joins holds its round. A thread may run a round of fibres of its own; a fibre never moves between threads.- A thread starts with every asynchronous signal blocked, so a signal the process is sent reaches the provider that reads it, through a signalfd or kqueue, and never a thread's default disposition.
- Every stack is prepared as a fibre's is, so a task that goes too deep traps
stack_exhaustedat the routine whose frame would not fit. Nothing is allocated and no guard page is mapped. - The first failure is kept and the rest are dropped; nothing cancels a thread. A start the system refuses fails
admitwithmem.out_of_memory, its errno kept in the group's handle forsystem.last_errno, and starts nothing. admitwaits for a free stack by joining a thread whose task has ended, or else the one that started first.
Each stack holds its record, at least 4 KiB for the task's own stack and, at its top, the area its thread starts on, which is the system's:
| System | Start area | Least stack |
|---|---|---|
| Linux, static | 2 KiB and the thread's own storage | about 6 KiB |
Linux, --link=libc | glibc's least: 16 KiB, 128 KiB on arm64 | about 20 KiB, 132 KiB on arm64 |
| Darwin | 16 KiB, in whole 16 KiB pages | 32 KiB |
| FreeBSD | 16 KiB | about 20 KiB |
A smaller backing fails group_over with mem.out_of_memory, before any thread starts. A static Linux program starts each thread with clone and waits on the word the kernel clears when it ends; under --link=libc, on Darwin and on FreeBSD the C library starts it with pthread_create on the area and pthread_join waits, and a FreeBSD program that starts one links libthr.
Items
public group: type = group_value
Threads a routine starts and waits for: count equal stacks cut from a backing the caller lends, each with its record at its base and the area its thread starts on at its top. Made by group_over, given tasks by admit, and waited for by join, which the routine making the group defers as it would a region's release: a group must be joined before its backing goes, and nothing else makes sure it is.
public admit: (state_type: type, inout g: group,
work: (inout state: state_type) -> none ! failure.reason,
escaping into g state: state_type)
-> none ! mem.out_of_memory
Start a task: wait until one of the group's stacks is free, joining a thread whose task has ended, or else the one that started first; copy state onto the top of that stack's own part, as a call passes an argument; prepare the stack; and start a thread whose first frame calls work with the copy. The thread begins with every asynchronous signal blocked, and only a fault's own unblocked (D521). Because each task's state lives on its own stack, one group holds tasks of different state types. The thread runs in parallel with the caller: everything sequenced before admit happens before the task's first action, and nothing after it does unless the two synchronize themselves (D227).
work's failure is kept by the group, the first one only, and join fails with it. A task with atoms of its own maps them to failure.reason before returning. state is kept in the group until the task ends, so what it refers to must outlive the group (D445). It fails mem.out_of_memory when the state leaves the stack less than 4 KiB for the task's own calls, and when the system refuses the thread, whose errno the group's handle then keeps for system.last_errno; a failed start starts nothing and leaves the stack free.
public group_over: (host: system.host, backing: []mut u8, count: usize)
-> (g: group from host, backing) ! mem.out_of_memory
A group of threads over host, whose last-error cell keeps the errno of a start the system refused. It cuts backing into count equal stacks and fails mem.out_of_memory when they would be smaller than least_stack(), before any thread starts. The group borrows the handle and the backing for as long as it is used [0790].
public join: (inout g: group) -> none ! failure.reason
Wait until every task admitted to g has ended, blocking the calling thread, whatever provider of fibres it runs under: a fibre that joins holds its round. Every thread's end happens before join returns (D227). The first failure any task had is join's, handed on once; the rest ran to their own ends regardless, since nothing cancels a thread. A group may be joined again, and given more tasks after it is.
public least_stack: () -> (bytes: usize)
The least bytes one stack of a group takes on this system: its record, 4 KiB for the task's own stack and the area the thread starts on, the system's least: 2 KiB and the thread's storage on a static Linux program, glibc's PTHREAD_STACK_MIN, 16 KiB on x86-64 and RV64 and 128 KiB on arm64, under --link=libc, and 16 KiB on Darwin and FreeBSD, where Darwin cuts every stack in whole 16 KiB pages (D519).
Example program
The program writes ordinary data, starts two threads that each read half of it and write their results, and reads every result after join, with no other synchronization of its own.
This complete program is one of the compiler's runtime tests. View source.
-- D227's two edges for hosted threads, and nothing else: the routine -- writes `published` with ordinary stores, admits two threads that each -- read their half and write `results` with ordinary stores, and reads -- every result once `join` has returned. No atomic, fence or lock of the -- program's own orders any of it, so a value read wrong on either side -- is an edge the thread facility did not supply. The threads run at -- once, each on a stack of the group's backing. import core/failure import hosted/system import hosted/thread -- How many words each thread reads and writes. half: usize = 512 mut published: [2 * half]u64 = zeroed mut results: [2 * half]u64 = zeroed part: type = struct first: usize count: usize end part -- Read the published words of its part and write their results. work: (inout state: part) -> none ! failure.reason = for index in state.first..<state.first + state.count do results[index] = published[index] * 3 + 1 end for end work -- Two stacks, with room for Darwin's whole 16 KiB pages. mut backing: [3 * 32768]u8 = zeroed run: (host: system.host) -> none ! failure.reason = mut workers := try thread.group_over(host, backing, 2) defer thread.join(workers) else none low: part = (first: 0, count: half) try thread.admit(workers, work, low) high: part = (first: half, count: half) try thread.admit(workers, work, high) try thread.join(workers) end run public main: () -> (code: i32) = code = 1 for index in usize(0)..<2 * half do published[index] = u64(index) * 7 + 5 end for run(system.new()) else none code = 2 for index in usize(0)..<2 * half do return when results[index] <> (u64(index) * 7 + 5) * 3 + 1 end for code = 42 end main