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
@@ -166,12 +166,105 @@ fn runFutexMode() void {
write("thread-futex: ok\n"); // the M4 verdict marker
}
// --- M5: mutex mode (bounded producer/consumer over Mutex + Condition) ------
const Mutex = runtime.Thread.Mutex;
const Condition = runtime.Thread.Condition;
const producers: u32 = 2;
const consumers: u32 = 2;
const per_producer: u32 = 1000;
const per_consumer: u32 = 1000; // producers*per_producer == consumers*per_consumer (balanced)
const total_items: u32 = producers * per_producer;
const ring_cap: usize = 8; // small, so producers block on full and consumers on empty
var ring: [ring_cap]u32 = undefined;
var ring_count: usize = 0;
var ring_head: usize = 0;
var ring_tail: usize = 0;
var pc_mutex = Mutex{};
var not_full = Condition{};
var not_empty = Condition{};
// Verified outside the lock: the checksum and tally of everything consumed.
var consumed_sum = std.atomic.Value(u64).init(0);
var consumed_count = std.atomic.Value(u32).init(0);
fn producer(base: u32) void {
var i: u32 = 0;
while (i < per_producer) : (i += 1) {
const item = base + i;
pc_mutex.lock();
while (ring_count == ring_cap) not_full.wait(&pc_mutex);
ring[ring_tail] = item;
ring_tail = (ring_tail + 1) % ring_cap;
ring_count += 1;
pc_mutex.unlock();
not_empty.signal();
}
}
fn consumer() void {
var i: u32 = 0;
while (i < per_consumer) : (i += 1) {
pc_mutex.lock();
while (ring_count == 0) not_empty.wait(&pc_mutex);
const item = ring[ring_head];
ring_head = (ring_head + 1) % ring_cap;
ring_count -= 1;
pc_mutex.unlock();
not_full.signal();
_ = consumed_sum.fetchAdd(item, .monotonic);
_ = consumed_count.fetchAdd(1, .monotonic);
}
}
fn runMutexMode() void {
write("thread-mutex: starting\n");
var threads: [producers + consumers]runtime.Thread = undefined;
var n: usize = 0;
var p: u32 = 0;
while (p < producers) : (p += 1) {
threads[n] = runtime.Thread.spawn(.{}, producer, .{p * per_producer}) catch {
write("thread-mutex: FAIL producer spawn\n");
return;
};
n += 1;
}
var c: u32 = 0;
while (c < consumers) : (c += 1) {
threads[n] = runtime.Thread.spawn(.{}, consumer, .{}) catch {
write("thread-mutex: FAIL consumer spawn\n");
return;
};
n += 1;
}
for (threads[0..n]) |t| t.join();
// Every item 0..total_items-1 was produced exactly once; if the mutex/condition are
// correct, each was consumed exactly once, so the checksum matches.
const expected_sum: u64 = @as(u64, total_items) * (total_items - 1) / 2;
if (consumed_count.load(.acquire) != total_items) {
write("thread-mutex: FAIL wrong number of items consumed\n");
return;
}
if (consumed_sum.load(.acquire) != expected_sum) {
write("thread-mutex: FAIL checksum mismatch (item lost or duplicated)\n");
return;
}
write("thread-mutex: ok\n"); // the M5 verdict marker
}
pub fn main(init: runtime.process.Init) void {
const mode = init.arguments.get(1) orelse "spawn";
if (std.mem.eql(u8, mode, "join")) {
runJoinMode();
} else if (std.mem.eql(u8, mode, "futex")) {
runFutexMode();
} else if (std.mem.eql(u8, mode, "mutex")) {
runMutexMode();
} else {
runSpawnMode();
}