Files
Zag/src/tests/mutex.zig
T
2026-08-01 22:43:32 -04:00

184 lines
5.2 KiB
Zig

//! Mutex stress test.
const std = @import("std");
const r = @import("root");
const ke = r.ke;
const ps = r.ps;
const nmutexes = 8;
const max_threads = 32;
const Guarded = struct {
mutex: ke.Mutex,
guard: usize,
count: u64,
};
var locks: [nmutexes]Guarded = undefined;
var totals: [max_threads]u64 = undefined;
var tds: [max_threads]*ps.Thread = undefined;
var is_parked: [max_threads]std.atomic.Value(bool) = undefined;
var nthreads: usize = 0;
var paused = std.atomic.Value(bool).init(false);
var parked = std.atomic.Value(usize).init(0);
fn rand(state: *u64) u64 {
var x = state.*;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
state.* = x;
return x;
}
fn delay_ms(ms: usize) void {
var timer: ke.Timer = undefined;
timer.init();
ke.timer.set(&timer, std.time.ns_per_ms * ms, .{});
_ = ke.wait.wait_one(&timer.hdr, null) catch unreachable;
}
fn worker(param: ?*anyopaque) void {
const id = @intFromPtr(param.?) - 1;
var seed = (ke.time.read_time() ^ (id *% 0x9e3779b97f4a7c15)) | 1;
var iter: u64 = 0;
while (true) : (iter += 1) {
if (paused.load(.acquire)) {
_ = parked.fetchAdd(1, .acq_rel);
is_parked[id].store(true, .release);
while (paused.load(.acquire)) delay_ms(1);
is_parked[id].store(false, .release);
_ = parked.fetchSub(1, .acq_rel);
}
const first: usize = @intCast(rand(&seed) % nmutexes);
const last = @min(first + 1 + rand(&seed) % 2, nmutexes);
for (first..last) |i| locks[i].mutex.acquire();
for (first..last) |i| {
if (@atomicLoad(usize, &locks[i].guard, .monotonic) != 0) {
@panic("mutex: guard set on entry, exclusion violated");
}
@atomicStore(usize, &locks[i].guard, id + 1, .monotonic);
}
if (rand(&seed) % 16 == 0) {
delay_ms(1);
} else {
for (0..rand(&seed) % 256) |_| std.atomic.spinLoopHint();
}
for (first..last) |i| {
const c = @atomicLoad(u64, &locks[i].count, .monotonic);
for (0..rand(&seed) % 64) |_| std.atomic.spinLoopHint();
@atomicStore(u64, &locks[i].count, c + 1, .monotonic);
if (@atomicLoad(usize, &locks[i].guard, .monotonic) != id + 1) {
@panic("mutex: guard changed while held, exclusion violated");
}
@atomicStore(usize, &locks[i].guard, 0, .monotonic);
}
var i = last;
while (i > first) : (i -= 1) locks[i - 1].mutex.release();
totals[id] += last - first;
if (iter % 64 == 0) delay_ms(1);
}
}
fn dump_stuck_workers() void {
for (0..nthreads) |i| {
if (is_parked[i].load(.acquire)) continue;
const td = &tds[i].kern;
const waiting = @atomicLoad(?*anyopaque, &td.waiting_on, .monotonic);
var lock_idx: isize = -1;
for (&locks, 0..) |*l, j| {
if (waiting == @as(?*anyopaque, @ptrCast(&l.mutex))) lock_idx = @intCast(j);
}
std.log.err("mutex: worker {} stuck: state {s}, prio {}/{}, inherited {}, waiting_on {x} (lock {})", .{
i,
@tagName(td.state.load(.monotonic)),
@atomicLoad(u8, &td.priority, .monotonic),
@atomicLoad(u8, &td.base_priority, .monotonic),
@atomicLoad(u8, &td.inherited_prio, .monotonic),
if (waiting) |w| @intFromPtr(w) else 0,
lock_idx,
});
}
}
/// Park all workers (they hold nothing at the loop top), then verify that
/// no increments were lost and no guard is stuck.
fn quiesce_verify() u64 {
paused.store(true, .release);
var spins: usize = 0;
while (parked.load(.acquire) != nthreads) : (spins += 1) {
if (spins > 10_000) {
dump_stuck_workers();
@panic("mutex: workers failed to park");
}
delay_ms(1);
}
var counted: u64 = 0;
var expected: u64 = 0;
for (&locks) |*l| {
if (@atomicLoad(usize, &l.guard, .monotonic) != 0) {
@panic("mutex: guard set at quiescence");
}
counted += @atomicLoad(u64, &l.count, .monotonic);
}
for (0..nthreads) |i| expected += @atomicLoad(u64, &totals[i], .monotonic);
if (counted != expected) {
std.log.err("mutex: lost updates: counted {}, expected {}", .{ counted, expected });
@panic("mutex: lost updates, exclusion or ordering violated");
}
paused.store(false, .release);
while (parked.load(.acquire) != 0) delay_ms(1);
return counted;
}
pub fn start(_: ?*anyopaque) void {
for (&locks) |*l| {
l.mutex = .init();
l.guard = 0;
l.count = 0;
}
nthreads = @max(4, @min(ke.ncpus * 2, max_threads));
for (0..nthreads) |i| {
totals[i] = 0;
is_parked[i] = .init(false);
const prio: u8 = @intCast(2 + (i * 5) % 29);
const t = ps.thread.create_kernel(prio, &worker, @ptrFromInt(i + 1)) catch @panic("oom");
tds[i] = t;
ke.sched.enqueue(&t.kern);
}
while (true) {
delay_ms(2000);
const count = quiesce_verify();
std.log.info("mutex: {} sections, no lost updates", .{count});
}
}