threads(M5): Mutex, Condition, and Semaphore over the futex

runtime.Thread.Mutex is the classic three-state futex mutex (unlocked/locked/
contended): the fast path is a single CAS and only a contended lock enters the
kernel. Condition is a futex sequence counter (wait/timedWait/signal/broadcast,
spurious wakeups allowed, use in a predicate loop); a signal racing the unlock
bumps the seq so it is never missed. Semaphore is permits guarded by
Mutex+Condition. All mirror std.Thread's shapes, ported onto runtime.Thread.Futex.

thread-test gains a mutex mode: 2 producers + 2 consumers move 2000 unique items
through an 8-slot ring (small enough that both sides block); the consumed
checksum and tally match exactly, proving the lock and condvars correct under
real cross-core contention.

Deferred with rationale (see docs/threading-plan.md): migrating join to a futex
completion word needs kernel clear-on-exit (else use-after-free munmapping a live
stack); host unit tests need a mockable Futex seam.

Gate thread-mutex PASS (3x); 17 guardrail/thread cases green; build + host tests
clean.
This commit is contained in:
2026-07-20 21:48:26 +01:00
parent b3a8147bd7
commit 1b33f48acd
5 changed files with 271 additions and 15 deletions
+98
View File
@@ -127,6 +127,104 @@ pub const Thread = struct {
_ = futexWake(@intFromPtr(ptr), max_waiters);
}
};
/// 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();
}
};
};
/// thread_spawn(entry, stack_top, arg, exit_endpoint) -> tid, or a wrapped error.