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:
@@ -147,6 +147,8 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void {
|
||||
threadJoinTest(boot_information);
|
||||
} else if (eql(case, "thread-futex")) {
|
||||
threadFutexTest(boot_information);
|
||||
} else if (eql(case, "thread-mutex")) {
|
||||
threadMutexTest(boot_information);
|
||||
} else if (eql(case, "args")) {
|
||||
argsTest(boot_information);
|
||||
} else if (eql(case, "init")) {
|
||||
@@ -1600,6 +1602,50 @@ fn threadFutexTest(boot_information: *const BootInformation) void {
|
||||
result();
|
||||
}
|
||||
|
||||
/// Mutex + Condition (docs/threading-plan.md M5): `thread-test` in mutex mode runs a
|
||||
/// bounded producer/consumer — P producers and C consumers over one `Mutex` and two
|
||||
/// `Condition`s move N unique items through a small ring. Every item is produced once;
|
||||
/// if the lock and condition variables are correct under real cross-core contention,
|
||||
/// the consumed checksum and tally match exactly (no lost or duplicated item, no
|
||||
/// overrun). The verdict marker is emitted only when both match.
|
||||
fn threadMutexTest(boot_information: *const BootInformation) void {
|
||||
log("DANOS-TEST-BEGIN: thread-mutex\n", .{});
|
||||
if (boot_information.initial_ramdisk_len == 0) {
|
||||
check("bootloader handed over an initial_ramdisk", false);
|
||||
result();
|
||||
return;
|
||||
}
|
||||
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
|
||||
const rd = initial_ramdisk.Reader.init(image) orelse {
|
||||
check("initial_ramdisk image is valid", false);
|
||||
result();
|
||||
return;
|
||||
};
|
||||
|
||||
var started = false;
|
||||
var i: u32 = 0;
|
||||
while (i < rd.count) : (i += 1) {
|
||||
const item = rd.entry(i) orelse continue;
|
||||
if (!eql(item.name, "thread-test")) continue;
|
||||
started = if (process.spawnProcess(item.blob, 4, &.{ "thread-test", "mutex" })) true else |_| false;
|
||||
break;
|
||||
}
|
||||
check("thread-test (mutex mode) spawned", started);
|
||||
|
||||
const ok_marker = "thread-mutex: ok";
|
||||
const fail_marker = "thread-mutex: FAIL";
|
||||
scheduler.setPriority(1);
|
||||
const deadline = architecture.millis() + 20000;
|
||||
while (architecture.millis() < deadline) {
|
||||
if (bufferHas(ok_marker) or bufferHas(fail_marker)) break;
|
||||
scheduler.yield();
|
||||
}
|
||||
scheduler.setPriority(4);
|
||||
|
||||
check("producer/consumer over Mutex+Condition moved every item exactly once", bufferHas(ok_marker) and !bufferHas(fail_marker));
|
||||
result();
|
||||
}
|
||||
|
||||
/// The full PID-1 path: the bootloader read /system/services/init off the boot volume and
|
||||
/// handed it over; load it as a user ELF and spawn it as a real ring-3 process
|
||||
/// — the same call the normal boot path makes — then confirm it beats. init
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user