threads(M11): RwLock, WaitGroup, and host-testable sync — Phase 2 done

runtime.Thread.RwLock (reader-preferring, lock/tryLock/unlock +
lockShared/tryLockShared/unlockShared) and WaitGroup (start/finish/wait), both on
the existing Mutex/Condition.

A compile-time Futex seam gated on builtin.os.tag: the futex syscalls on danos, a
spin+yield mock off-target (Zig 0.16 has no std.Thread.Futex; wake is a no-op
since the state machines re-check). thread.zig is wired into zig build test, so
Mutex/RwLock/WaitGroup run as host unit tests with real std.Thread threads (test
blocks compile only under test, so std.Thread there is fine on freestanding).

thread-rwlock QEMU case: 2 writers set both halves of a value under the exclusive
lock while 3 readers check they match under the shared lock; zero half-write
observations across ~150k reads.

Marks Phase 2 (M7-M11) built. threading.md/threading-plan.md status updated.

Gate: host zig build test covers the sync primitives; thread-rwlock PASS (3x);
full Done gate 26/26 (whole thread-* suite + guardrail); build clean.
This commit is contained in:
2026-07-21 00:09:48 +01:00
parent c7e9b5a4f6
commit a4e44e8f31
7 changed files with 376 additions and 22 deletions
+44
View File
@@ -157,6 +157,8 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void {
taskReapTest(boot_information);
} else if (eql(case, "thread-tls")) {
threadTlsTest(boot_information);
} else if (eql(case, "thread-rwlock")) {
threadRwlockTest(boot_information);
} else if (eql(case, "args")) {
argsTest(boot_information);
} else if (eql(case, "init")) {
@@ -1785,6 +1787,48 @@ fn threadTlsTest(boot_information: *const BootInformation) void {
result();
}
/// RwLock (docs/threading-plan.md M11): `thread-test` in rwlock mode runs writers that set
/// two halves of a value under the exclusive lock and readers that check the halves match
/// under the shared lock. If the reader/writer lock were wrong, a reader would observe a
/// half-written value; zero violations across many reads → the lock holds.
fn threadRwlockTest(boot_information: *const BootInformation) void {
log("DANOS-TEST-BEGIN: thread-rwlock\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", "rwlock" })) true else |_| false;
break;
}
check("thread-test (rwlock mode) spawned", started);
const ok_marker = "thread-rwlock: ok";
const fail_marker = "thread-rwlock: 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("readers/writers over an RwLock never observed a half-written value", bufferHas(ok_marker) and !bufferHas(fail_marker));
result();
}
/// The task reaper (docs/threading-plan.md M8): a dead task's kernel stack used to be
/// leaked ("no reaper yet"). Spawn and kill many ring-3 processes and confirm the total
/// kernel-stack bytes return to baseline — every stack reclaimed, no leak. (Threads exit
@@ -410,6 +410,65 @@ fn runTlsMode() void {
}
}
// --- M11: rwlock mode (readers/writers over an RwLock) ----------------------
const RwLock = runtime.Thread.RwLock;
var rwlock = RwLock{};
var rw_a: u64 = 0;
var rw_b: u64 = 0; // invariant while any lock is held: rw_a == rw_b
var rw_stop = std.atomic.Value(u32).init(0);
var rw_violations = std.atomic.Value(u32).init(0);
var rw_reads = std.atomic.Value(u64).init(0);
fn rwWriter() void {
var v: u64 = 1;
while (rw_stop.load(.acquire) == 0) : (v +%= 1) {
rwlock.lock(); // exclusive: no reader may observe the gap between the two writes
rw_a = v;
rw_b = v;
rwlock.unlock();
}
}
fn rwReader() void {
const reads: u64 = 50_000;
var i: u64 = 0;
while (i < reads) : (i += 1) {
rwlock.lockShared();
if (rw_a != rw_b) _ = rw_violations.fetchAdd(1, .monotonic); // saw a half-write!
rwlock.unlockShared();
}
_ = rw_reads.fetchAdd(reads, .monotonic);
}
fn runRwlockMode() void {
write("thread-rwlock: starting\n");
var writers: [2]runtime.Thread = undefined;
var readers: [3]runtime.Thread = undefined;
for (&writers) |*w| {
w.* = runtime.Thread.spawn(.{}, rwWriter, .{}) catch {
write("thread-rwlock: FAIL spawn\n");
return;
};
}
for (&readers) |*r| {
r.* = runtime.Thread.spawn(.{}, rwReader, .{}) catch {
write("thread-rwlock: FAIL spawn\n");
return;
};
}
for (readers) |r| r.join();
rw_stop.store(1, .release); // readers done → stop the writers
for (writers) |w| w.join();
if (rw_violations.load(.acquire) == 0 and rw_reads.load(.acquire) > 0) {
write("thread-rwlock: ok\n"); // the M11 verdict marker
} else {
write("thread-rwlock: FAIL reader observed a half-written value\n");
}
}
pub fn main(init: runtime.process.Init) void {
const mode = init.arguments.get(1) orelse "spawn";
if (std.mem.eql(u8, mode, "join")) {
@@ -424,6 +483,8 @@ pub fn main(init: runtime.process.Init) void {
runAllocMode();
} else if (std.mem.eql(u8, mode, "tls")) {
runTlsMode();
} else if (std.mem.eql(u8, mode, "rwlock")) {
runRwlockMode();
} else {
runSpawnMode();
}