//! `runtime.Thread` — threads for danos, shaped like Zig's `std.Thread` but built on //! danos's private thread ABI (docs/threading.md). Several tasks share one address //! space; `spawn` starts one, the kernel delivers the closure pointer in the new //! thread's rdi, a plain Zig trampoline runs the user function and calls `thread_exit`, //! and `join` blocks on the thread's exit notification. See docs/threading.md for why //! this mirrors `std.Thread`'s API rather than being the literal type. //! //! The closure (the function's captured args) lives at the **top of the thread's own //! stack**, not the heap — each thread's stack is private, so there is no shared-heap //! concurrency in the spawn/join machinery (the runtime heap is not yet thread-safe). //! A binary must be built multi-threaded (`addThreadedUserBinary`) before it may spawn. const std = @import("std"); const builtin = @import("builtin"); const abi = @import("abi"); const sc = @import("system-call.zig"); const system = @import("system.zig"); /// True in a real danos binary; false when this module is compiled for host unit tests. /// The `Futex` seam and the test blocks below branch on it so the lock/condvar state /// machines can be exercised on the host against `std.Thread.Futex` (docs/threading-plan.md /// M11), while the danos build uses the futex syscalls. const on_danos = builtin.os.tag == .freestanding; /// A thread stack, if the caller does not override it. 64 KiB of mmap'd, zeroed pages. pub const default_stack_size: usize = 64 * 1024; /// Bytes reserved at the top of each thread's stack for its per-thread TLS block (the /// self-pointer plus scratch slots reachable via `%fs`). docs/threading-plan.md M10. const tls_block_size: usize = 64; pub const Thread = struct { /// The kernel task id of the spawned thread — what `join` waits on. tid: u32, /// The mmap'd stack, reclaimed by `join` (or at process exit after `detach`). stack_base: usize, stack_size: usize, pub const Id = u32; pub const SpawnConfig = struct { /// Bytes of stack, rounded up to whole pages by the kernel's mmap. stack_size: usize = default_stack_size, }; pub const SpawnError = error{ /// The kernel refused the thread, the stack mmap failed, or no endpoint was free. SystemResources, }; /// Start `function(args...)` on a new thread sharing this address space. Mirrors /// `std.Thread.spawn`. The thread's return value is discarded (as in `std.Thread`); /// return data through shared state. pub fn spawn(config: SpawnConfig, comptime function: anytype, args: anytype) SpawnError!Thread { const Args = @TypeOf(args); const Closure = struct { tls_base: usize, args: Args, /// Entered directly by the kernel with `self` in rdi (C ABI). Establishes this /// thread's TLS pointer, runs the user function, then ends the thread. fn entry(self_addr: usize) callconv(.c) noreturn { const self: *@This() = @ptrFromInt(self_addr); setThreadPointer(self.tls_base); // per-thread thread pointer before any user code @call(.auto, function, self.args); exitThread(); } }; const base = system.mmap(config.stack_size, system.PROT_READ | system.PROT_WRITE); if (system.mmapFailed(base)) return error.SystemResources; // Top of the thread's own stack, downward: the closure, then a small per-thread TLS // block (the thread pointer points here; slot 0 is the variant-II self-pointer, the rest is // scratch for user TLS), then the stack proper (rsp starts below the TLS block, so // the growing stack never overwrites either). var closure_addr = (base + config.stack_size) - @sizeOf(Closure); closure_addr &= ~@as(usize, @alignOf(Closure) - 1); // align the closure down const tls_base = (closure_addr - tls_block_size) & ~@as(usize, 15); const tls: [*]usize = @ptrFromInt(tls_base); tls[0] = tls_base; // self-pointer (fs:0), as the x86_64 TLS ABI expects const closure: *Closure = @ptrFromInt(closure_addr); closure.* = .{ .tls_base = tls_base, .args = args }; var stack_top = tls_base & ~@as(usize, 15); // 16-align below the TLS block stack_top -= 8; // ...then rsp % 16 == 8 at the C entry const tid = threadSpawn(@intFromPtr(&Closure.entry), stack_top, closure_addr); if (threadSpawnFailed(tid)) { _ = system.munmap(base, config.stack_size); return error.SystemResources; } return .{ .tid = @intCast(tid), .stack_base = base, .stack_size = config.stack_size }; } /// Block until this thread finishes, then reclaim its stack. Mirrors /// `std.Thread.join`. The exit endpoint is private to this thread, so the first /// child-exit notification on it is this thread's. pub fn join(self: Thread) void { _ = sc.systemCall1(.thread_join, self.tid); // block until the thread has exited _ = system.munmap(self.stack_base, self.stack_size); // reclaim its (now-vacated) stack } /// Relinquish the right to join: never wait for or reclaim this thread. Its stack is /// reclaimed at process exit (docs/threading-plan.md M3 — kernel-reaper stack reclaim /// for detached threads is a later refinement). Mirrors `std.Thread.detach`. pub fn detach(self: Thread) void { _ = self; } /// The calling thread's id (its kernel task id). Mirrors `std.Thread.getCurrentId`. pub fn getCurrentId() Id { return @intCast(sc.systemCall0(.thread_self)); } /// The dense 0-based index of the core the calling thread is running on. A danos /// extension beyond `std.Thread`, used to observe genuine cross-core parallelism. pub fn currentCore() Id { return @intCast(sc.systemCall0(.current_core)); } /// `std.Thread.Futex`-shaped block/wake on a `u32` atomic — the primitive the /// blocking `Mutex`/`Condition`/`Semaphore` are built on. Waiters park in the /// kernel (no busy-wait), so an idle core still halts (docs/halting.md). pub const Futex = struct { /// Block while `ptr.* == expect`. Returns when woken by `wake`, or promptly if /// the value already differs (safe against spurious returns, as in std): the /// caller re-checks its condition in a loop. pub fn wait(ptr: *const std.atomic.Value(u32), expect: u32) void { if (comptime on_danos) { _ = futexWait(@intFromPtr(ptr), expect, 0); } else { // Host unit-test mock: spin+yield until the value changes (`wake` is a // no-op — the callers re-check their condition in a loop anyway). Correct, // if busy; fine for the state-machine tests. while (ptr.load(.acquire) == expect) std.Thread.yield() catch {}; } } /// As `wait`, but returns `error.Timeout` if `timeout_ns` elapses first. pub fn timedWait(ptr: *const std.atomic.Value(u32), expect: u32, timeout_ns: u64) error{Timeout}!void { if (comptime on_danos) { if (futexWait(@intFromPtr(ptr), expect, timeout_ns) == abi.futex_timed_out) return error.Timeout; } else { var spins: u64 = 0; const limit = timeout_ns / 1000 + 1; while (ptr.load(.acquire) == expect) : (spins += 1) { if (spins >= limit) return error.Timeout; std.Thread.yield() catch {}; } } } /// Wake up to `max_waiters` threads blocked on `ptr`. pub fn wake(ptr: *const std.atomic.Value(u32), max_waiters: u32) void { if (comptime on_danos) { _ = futexWake(@intFromPtr(ptr), max_waiters); } else { // host mock: spin-waiters re-check their condition, so no wake is needed. } } }; /// A mutual-exclusion lock, `std.Thread.Mutex`-shaped. The classic three-state /// futex mutex (unlocked / locked / contended): the fast path is a single CAS, and /// only a contended lock ever enters the kernel. pub const Mutex = struct { state: std.atomic.Value(u32) = std.atomic.Value(u32).init(unlocked), const unlocked: u32 = 0; const locked: u32 = 1; const contended: u32 = 2; /// Try to take the lock without blocking; returns whether it was acquired. pub fn tryLock(m: *Mutex) bool { return m.state.cmpxchgStrong(unlocked, locked, .acquire, .monotonic) == null; } /// Acquire the lock, blocking in the kernel while it is contended. pub fn lock(m: *Mutex) void { if (m.state.cmpxchgStrong(unlocked, locked, .acquire, .monotonic) != null) m.lockSlow(); } fn lockSlow(m: *Mutex) void { @branchHint(.cold); // Mark the lock contended and take it as soon as it falls unlocked; park on // the futex while it stays contended. Marking contended may cause a spurious // wake on unlock (harmless), never a missed one. while (m.state.swap(contended, .acquire) != unlocked) { Futex.wait(&m.state, contended); } } /// Release the lock; wake one waiter if the lock was contended. pub fn unlock(m: *Mutex) void { if (m.state.swap(unlocked, .release) == contended) Futex.wake(&m.state, 1); } }; /// A condition variable, `std.Thread.Condition`-shaped. Spurious wakeups are /// allowed — always wait in a predicate loop with the mutex held. Built on a futex /// sequence counter: a waiter samples the seq, drops the mutex, and parks until the /// seq changes (a signal that races the unlock bumps the seq, so it is not missed). pub const Condition = struct { seq: std.atomic.Value(u32) = std.atomic.Value(u32).init(0), /// Atomically release `mutex` and block until signalled, then re-acquire it. pub fn wait(c: *Condition, mutex: *Mutex) void { const seq = c.seq.load(.acquire); mutex.unlock(); Futex.wait(&c.seq, seq); mutex.lock(); } /// As `wait`, but returns `error.Timeout` if `timeout_ns` elapses first. The /// mutex is re-acquired either way. pub fn timedWait(c: *Condition, mutex: *Mutex, timeout_ns: u64) error{Timeout}!void { const seq = c.seq.load(.acquire); mutex.unlock(); const timed_out = if (Futex.timedWait(&c.seq, seq, timeout_ns)) |_| false else |_| true; mutex.lock(); if (timed_out) return error.Timeout; } /// Wake one waiter. pub fn signal(c: *Condition) void { _ = c.seq.fetchAdd(1, .release); Futex.wake(&c.seq, 1); } /// Wake all waiters. pub fn broadcast(c: *Condition) void { _ = c.seq.fetchAdd(1, .release); Futex.wake(&c.seq, std.math.maxInt(u32)); } }; /// A counting semaphore, `std.Thread.Semaphore`-shaped: a permit count guarded by a /// `Mutex` + `Condition`. pub const Semaphore = struct { mutex: Mutex = .{}, cond: Condition = .{}, permits: usize = 0, /// Take a permit, blocking until one is available. pub fn wait(s: *Semaphore) void { s.mutex.lock(); defer s.mutex.unlock(); while (s.permits == 0) s.cond.wait(&s.mutex); s.permits -= 1; } /// Return a permit and wake a waiter. pub fn post(s: *Semaphore) void { s.mutex.lock(); defer s.mutex.unlock(); s.permits += 1; s.cond.signal(); } }; /// A reader/writer lock, `std.Thread.RwLock`-shaped: many concurrent readers OR one /// exclusive writer. Reader-preferring (a steady stream of readers can delay a writer), /// built on `Mutex` + `Condition` over a signed state: `>0` = that many readers hold /// it, `-1` = a writer holds it, `0` = free. pub const RwLock = struct { mutex: Mutex = .{}, cond: Condition = .{}, state: i64 = 0, /// Acquire shared (read) access, blocking while a writer holds the lock. pub fn lockShared(rw: *RwLock) void { rw.mutex.lock(); defer rw.mutex.unlock(); while (rw.state < 0) rw.cond.wait(&rw.mutex); rw.state += 1; } /// Try to acquire shared access without blocking. pub fn tryLockShared(rw: *RwLock) bool { rw.mutex.lock(); defer rw.mutex.unlock(); if (rw.state < 0) return false; rw.state += 1; return true; } /// Release shared access; wake a waiting writer once the last reader leaves. pub fn unlockShared(rw: *RwLock) void { rw.mutex.lock(); defer rw.mutex.unlock(); rw.state -= 1; if (rw.state == 0) rw.cond.broadcast(); } /// Acquire exclusive (write) access, blocking until no readers or writer remain. pub fn lock(rw: *RwLock) void { rw.mutex.lock(); defer rw.mutex.unlock(); while (rw.state != 0) rw.cond.wait(&rw.mutex); rw.state = -1; } /// Try to acquire exclusive access without blocking. pub fn tryLock(rw: *RwLock) bool { rw.mutex.lock(); defer rw.mutex.unlock(); if (rw.state != 0) return false; rw.state = -1; return true; } /// Release exclusive access; wake all waiters (they re-check their condition). pub fn unlock(rw: *RwLock) void { rw.mutex.lock(); defer rw.mutex.unlock(); rw.state = 0; rw.cond.broadcast(); } }; /// A `std.Thread.WaitGroup`-shaped counter: `start` before spawning work, `finish` as /// each unit completes, `wait` blocks until the count returns to zero. pub const WaitGroup = struct { mutex: Mutex = .{}, cond: Condition = .{}, counter: usize = 0, /// Register one pending unit of work. pub fn start(wg: *WaitGroup) void { wg.mutex.lock(); defer wg.mutex.unlock(); wg.counter += 1; } /// Mark one unit done; wake waiters if that was the last. pub fn finish(wg: *WaitGroup) void { wg.mutex.lock(); defer wg.mutex.unlock(); wg.counter -= 1; if (wg.counter == 0) wg.cond.broadcast(); } /// Block until every started unit has finished. pub fn wait(wg: *WaitGroup) void { wg.mutex.lock(); defer wg.mutex.unlock(); while (wg.counter != 0) wg.cond.wait(&wg.mutex); } }; }; /// thread_spawn(entry, stack_top, arg, exit_endpoint) -> tid, or a wrapped error. fn threadSpawn(entry: usize, stack_top: usize, arg: usize) usize { const exit_endpoint: usize = @intCast(abi.no_cap); // join uses thread_join, not an endpoint return sc.systemCall4(.thread_spawn, entry, stack_top, arg, exit_endpoint); } /// The kernel returns a real (small) task id on success and a wrapped `-1` on failure; /// no valid task id ever exceeds a u32. inline fn threadSpawnFailed(ret: usize) bool { return ret > std.math.maxInt(u32); } /// End the calling thread. Never returns. fn exitThread() noreturn { _ = sc.systemCall0(.thread_exit); unreachable; } /// Set the calling thread's FS base (its user TLS thread pointer). fn setThreadPointer(addr: usize) void { _ = sc.systemCall1(.set_thread_pointer, addr); } /// futex_wait(addr, expect, timeout_ns) -> status (abi.futex_*). fn futexWait(addr: usize, expect: u32, timeout_ns: u64) usize { return sc.systemCall3(.futex_wait, addr, expect, timeout_ns); } /// futex_wake(addr, count) -> number woken. fn futexWake(addr: usize, count: u32) usize { return sc.systemCall2(.futex_wake, addr, count); } // --- host unit tests (docs/threading-plan.md M11) --------------------------- // // These run under `zig build test` on the host: the `Futex` seam above uses // `std.Thread.Futex` off-danos, so the lock/condvar state machines can be exercised by // real host threads. They are never compiled into a danos binary (test blocks only build // under test), so their `std.Thread` use is fine even though `std.Thread` is unavailable // on the freestanding target. test "Mutex serialises concurrent increments across host threads" { var m: Thread.Mutex = .{}; var counter: u64 = 0; const workers = 8; const per = 20_000; const Ctx = struct { m: *Thread.Mutex, c: *u64, fn run(ctx: @This()) void { var i: usize = 0; while (i < per) : (i += 1) { ctx.m.lock(); ctx.c.* += 1; ctx.m.unlock(); } } }; var handles: [workers]std.Thread = undefined; for (&handles) |*h| h.* = try std.Thread.spawn(.{}, Ctx.run, .{Ctx{ .m = &m, .c = &counter }}); for (handles) |h| h.join(); try std.testing.expectEqual(@as(u64, workers * per), counter); } test "RwLock never lets a reader observe a half-written pair" { var rw: Thread.RwLock = .{}; var a: u64 = 0; var b: u64 = 0; // invariant while a lock is held: a == b var stop = std.atomic.Value(bool).init(false); var ok = std.atomic.Value(bool).init(true); const Writer = struct { rw: *Thread.RwLock, a: *u64, b: *u64, stop: *std.atomic.Value(bool), fn run(w: @This()) void { var v: u64 = 1; while (!w.stop.load(.acquire)) : (v +%= 1) { w.rw.lock(); w.a.* = v; // update both halves under the exclusive lock... w.b.* = v; w.rw.unlock(); } } }; const Reader = struct { rw: *Thread.RwLock, a: *u64, b: *u64, ok: *std.atomic.Value(bool), fn run(r: @This()) void { var i: usize = 0; while (i < 200_000) : (i += 1) { r.rw.lockShared(); if (r.a.* != r.b.*) r.ok.store(false, .release); // ...so a reader must never see them differ r.rw.unlockShared(); } } }; var writers: [2]std.Thread = undefined; for (&writers) |*w| w.* = try std.Thread.spawn(.{}, Writer.run, .{Writer{ .rw = &rw, .a = &a, .b = &b, .stop = &stop }}); var readers: [4]std.Thread = undefined; for (&readers) |*rd| rd.* = try std.Thread.spawn(.{}, Reader.run, .{Reader{ .rw = &rw, .a = &a, .b = &b, .ok = &ok }}); for (readers) |rd| rd.join(); stop.store(true, .release); for (writers) |w| w.join(); try std.testing.expect(ok.load(.acquire)); } test "WaitGroup blocks until every started unit finishes" { var wg: Thread.WaitGroup = .{}; var done = std.atomic.Value(u32).init(0); const n = 6; const Ctx = struct { wg: *Thread.WaitGroup, done: *std.atomic.Value(u32), fn run(c: @This()) void { _ = c.done.fetchAdd(1, .monotonic); c.wg.finish(); } }; var i: usize = 0; while (i < n) : (i += 1) wg.start(); var handles: [n]std.Thread = undefined; for (&handles) |*h| h.* = try std.Thread.spawn(.{}, Ctx.run, .{Ctx{ .wg = &wg, .done = &done }}); wg.wait(); // must not return until all n finished try std.testing.expectEqual(@as(u32, n), done.load(.acquire)); for (handles) |h| h.join(); }