authorgravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2025-12-15 17:07:50+00:00
committergravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2025-12-21 13:07:03+00:00
logb4ee54b5804b8afb0387a36c913a2cd8cf595409
treeee9dd3e02280bdf424772f097b1fb917bdd89eef
parent6ed5b620507aa4589d6c6fe3a5e48e166d48b3d2
signaturelock-open Commit is signed but in an unrecognized format.

std.Io: reimplement Mutex and Condition atop futex

This work was partially cherry-picked from Andrew's WIP std.fs branch. However, I also analyzed and simplified the Mutex and Condition implementations, and brought them in line with modern Zig style. Co-authored-by: Andrew Kelley <andrew@ziglang.org>

4 files changed, 145 insertions(+), 269 deletions(-)

lib/std/Build/Fuzz.zig+1-1
......@@ -124,7 +124,7 @@ pub fn init(
124124 .coverage_files = .empty,
125125 .coverage_mutex = .init,
126126 .queue_mutex = .init,
127 .queue_cond = .{},
127 .queue_cond = .init,
128128 .msg_queue = .empty,
129129 };
130130}
lib/std/Build/WebServer.zig+2-2
......@@ -122,8 +122,8 @@ pub fn init(opts: Options) WebServer {
122122 .update_id = .init(0),
123123
124124 .runner_request_mutex = .init,
125 .runner_request_ready_cond = .{},
126 .runner_request_empty_cond = .{},
125 .runner_request_ready_cond = .init,
126 .runner_request_empty_cond = .init,
127127 .runner_request = null,
128128 };
129129}
lib/std/Io.zig+142-66
......@@ -658,14 +658,6 @@ pub const VTable = struct {
658658 futexWaitUncancelable: *const fn (?*anyopaque, ptr: *const u32, expected: u32) void,
659659 futexWake: *const fn (?*anyopaque, ptr: *const u32, max_waiters: u32) void,
660660
661 mutexLock: *const fn (?*anyopaque, prev_state: Mutex.State, mutex: *Mutex) Cancelable!void,
662 mutexLockUncancelable: *const fn (?*anyopaque, prev_state: Mutex.State, mutex: *Mutex) void,
663 mutexUnlock: *const fn (?*anyopaque, prev_state: Mutex.State, mutex: *Mutex) void,
664
665 conditionWait: *const fn (?*anyopaque, cond: *Condition, mutex: *Mutex) Cancelable!void,
666 conditionWaitUncancelable: *const fn (?*anyopaque, cond: *Condition, mutex: *Mutex) void,
667 conditionWake: *const fn (?*anyopaque, cond: *Condition, wake: Condition.Wake) void,
668
669661 dirMake: *const fn (?*anyopaque, Dir, sub_path: []const u8, Dir.Mode) Dir.MakeError!void,
670662 dirMakePath: *const fn (?*anyopaque, Dir, sub_path: []const u8, Dir.Mode) Dir.MakeError!void,
671663 dirMakeOpenPath: *const fn (?*anyopaque, Dir, sub_path: []const u8, Dir.OpenOptions) Dir.MakeOpenPathError!Dir,
......@@ -1215,99 +1207,183 @@ pub fn futexWake(io: Io, comptime T: type, ptr: *align(@alignOf(u32)) const T, m
12151207}
12161208
12171209pub const Mutex = struct {
1218 state: State,
1210 state: std.atomic.Value(State),
12191211
1220 pub const State = enum(usize) {
1221 locked_once = 0b00,
1222 unlocked = 0b01,
1223 contended = 0b10,
1224 /// contended
1225 _,
1212 pub const init: Mutex = .{ .state = .init(.unlocked) };
12261213
1227 pub fn isUnlocked(state: State) bool {
1228 return @intFromEnum(state) & @intFromEnum(State.unlocked) == @intFromEnum(State.unlocked);
1229 }
1214 const State = enum(u32) {
1215 unlocked,
1216 locked_once,
1217 contended,
12301218 };
12311219
1232 pub const init: Mutex = .{ .state = .unlocked };
1233
1234 pub fn tryLock(mutex: *Mutex) bool {
1235 const prev_state: State = @enumFromInt(@atomicRmw(
1236 usize,
1237 @as(*usize, @ptrCast(&mutex.state)),
1238 .And,
1239 ~@intFromEnum(State.unlocked),
1220 pub fn tryLock(m: *Mutex) bool {
1221 switch (m.state.cmpxchgWeak(
1222 .unlocked,
1223 .locked_once,
12401224 .acquire,
1241 ));
1242 return prev_state.isUnlocked();
1225 .monotonic,
1226 ) orelse return true) {
1227 .unlocked => unreachable,
1228 .locked_once, .contended => return false,
1229 }
12431230 }
12441231
1245 pub fn lock(mutex: *Mutex, io: std.Io) Cancelable!void {
1246 const prev_state: State = @enumFromInt(@atomicRmw(
1247 usize,
1248 @as(*usize, @ptrCast(&mutex.state)),
1249 .And,
1250 ~@intFromEnum(State.unlocked),
1232 pub fn lock(m: *Mutex, io: Io) Cancelable!void {
1233 const initial_state = m.state.cmpxchgWeak(
1234 .unlocked,
1235 .locked_once,
12511236 .acquire,
1252 ));
1253 if (prev_state.isUnlocked()) {
1237 .monotonic,
1238 ) orelse {
12541239 @branchHint(.likely);
12551240 return;
1241 };
1242 if (initial_state == .contended) {
1243 try io.futexWait(State, &m.state.raw, .contended);
1244 }
1245 while (m.state.swap(.contended, .acquire) != .unlocked) {
1246 try io.futexWait(State, &m.state.raw, .contended);
12561247 }
1257 return io.vtable.mutexLock(io.userdata, prev_state, mutex);
12581248 }
12591249
1260 /// Same as `lock` but cannot be canceled.
1261 pub fn lockUncancelable(mutex: *Mutex, io: std.Io) void {
1262 const prev_state: State = @enumFromInt(@atomicRmw(
1263 usize,
1264 @as(*usize, @ptrCast(&mutex.state)),
1265 .And,
1266 ~@intFromEnum(State.unlocked),
1250 pub fn lockUncancelable(m: *Mutex, io: Io) void {
1251 const initial_state = m.state.cmpxchgWeak(
1252 .unlocked,
1253 .locked_once,
12671254 .acquire,
1268 ));
1269 if (prev_state.isUnlocked()) {
1255 .monotonic,
1256 ) orelse {
12701257 @branchHint(.likely);
12711258 return;
1259 };
1260 if (initial_state == .contended) {
1261 io.futexWaitUncancelable(State, &m.state.raw, .contended);
1262 }
1263 while (m.state.swap(.contended, .acquire) != .unlocked) {
1264 io.futexWaitUncancelable(State, &m.state.raw, .contended);
12721265 }
1273 return io.vtable.mutexLockUncancelable(io.userdata, prev_state, mutex);
12741266 }
12751267
1276 pub fn unlock(mutex: *Mutex, io: std.Io) void {
1277 const prev_state = @cmpxchgWeak(State, &mutex.state, .locked_once, .unlocked, .release, .acquire) orelse {
1278 @branchHint(.likely);
1279 return;
1280 };
1281 assert(prev_state != .unlocked); // mutex not locked
1282 return io.vtable.mutexUnlock(io.userdata, prev_state, mutex);
1268 pub fn unlock(m: *Mutex, io: Io) void {
1269 switch (m.state.swap(.unlocked, .release)) {
1270 .unlocked => unreachable,
1271 .locked_once => {},
1272 .contended => {
1273 @branchHint(.unlikely);
1274 io.futexWake(State, &m.state.raw, 1);
1275 },
1276 }
12831277 }
12841278};
12851279
12861280pub const Condition = struct {
1287 state: u64 = 0,
1281 state: std.atomic.Value(State),
1282 /// Incremented whenever the condition is signaled
1283 epoch: std.atomic.Value(u32),
1284
1285 const State = packed struct(u32) {
1286 waiters: u16,
1287 signals: u16,
1288 };
1289
1290 pub const init: Condition = .{
1291 .state = .init(.{ .waiters = 0, .signals = 0 }),
1292 .epoch = .init(0),
1293 };
12881294
12891295 pub fn wait(cond: *Condition, io: Io, mutex: *Mutex) Cancelable!void {
1290 return io.vtable.conditionWait(io.userdata, cond, mutex);
1296 try waitInner(cond, io, mutex, false);
12911297 }
12921298
12931299 pub fn waitUncancelable(cond: *Condition, io: Io, mutex: *Mutex) void {
1294 return io.vtable.conditionWaitUncancelable(io.userdata, cond, mutex);
1300 waitInner(cond, io, mutex, true) catch |err| switch (err) {
1301 error.Canceled => unreachable,
1302 };
1303 }
1304
1305 fn waitInner(cond: *Condition, io: Io, mutex: *Mutex, uncancelable: bool) Cancelable!void {
1306 var epoch = cond.epoch.load(.acquire); // `.acquire` to ensure ordered before state load
1307
1308 {
1309 const prev_state = cond.state.fetchAdd(.{ .waiters = 1, .signals = 0 }, .monotonic);
1310 assert(prev_state.waiters < math.maxInt(u16)); // overflow caused by too many waiters
1311 }
1312
1313 mutex.unlock(io);
1314 defer mutex.lockUncancelable(io);
1315
1316 while (true) {
1317 const result = if (uncancelable)
1318 io.futexWaitUncancelable(u32, &cond.epoch.raw, epoch)
1319 else
1320 io.futexWait(u32, &cond.epoch.raw, epoch);
1321
1322 epoch = cond.epoch.load(.acquire); // `.acquire` to ensure ordered before `state` laod
1323
1324 // Even on error, try to consume a pending signal first. Otherwise a race might
1325 // cause a signal to get stuck in the state with no corresponding waiter.
1326 {
1327 var prev_state = cond.state.load(.monotonic);
1328 while (prev_state.signals > 0) {
1329 prev_state = cond.state.cmpxchgWeak(prev_state, .{
1330 .waiters = prev_state.waiters - 1,
1331 .signals = prev_state.signals - 1,
1332 }, .acquire, .monotonic) orelse {
1333 // We successfully consumed a signal.
1334 return;
1335 };
1336 }
1337 }
1338
1339 // There are no more signals available; this was a spurious wakeup or an error. If it
1340 // was an error, we will remove ourselves as a waiter and return that error. Otherwise,
1341 // we'll loop back to the futex wait.
1342 result catch |err| {
1343 const prev_state = cond.state.fetchSub(.{ .waiters = 1, .signals = 0 }, .monotonic);
1344 assert(prev_state.waiters > 0); // underflow caused by illegal state
1345 return err;
1346 };
1347 }
12951348 }
12961349
12971350 pub fn signal(cond: *Condition, io: Io) void {
1298 io.vtable.conditionWake(io.userdata, cond, .one);
1351 var prev_state = cond.state.load(.monotonic);
1352 while (prev_state.waiters > prev_state.signals) {
1353 @branchHint(.unlikely);
1354 prev_state = cond.state.cmpxchgWeak(prev_state, .{
1355 .waiters = prev_state.waiters,
1356 .signals = prev_state.signals + 1,
1357 }, .release, .monotonic) orelse {
1358 // Update the epoch to tell the waiting threads that there are new signals for them.
1359 // Note that a waiting thread could miss a take if *exactly* (1<<32)-1 wakes happen
1360 // between it observing the epoch and sleeping on it, but this is extraordinarily
1361 // unlikely due to the precise number of calls required.
1362 _ = cond.epoch.fetchAdd(1, .release); // `.release` to ensure ordered after `state` update
1363 io.futexWake(u32, &cond.epoch.raw, 1);
1364 return;
1365 };
1366 }
12991367 }
13001368
13011369 pub fn broadcast(cond: *Condition, io: Io) void {
1302 io.vtable.conditionWake(io.userdata, cond, .all);
1370 var prev_state = cond.state.load(.monotonic);
1371 while (prev_state.waiters > prev_state.signals) {
1372 @branchHint(.unlikely);
1373 prev_state = cond.state.cmpxchgWeak(prev_state, .{
1374 .waiters = prev_state.waiters,
1375 .signals = prev_state.waiters,
1376 }, .release, .monotonic) orelse {
1377 // Update the epoch to tell the waiting threads that there are new signals for them.
1378 // Note that a waiting thread could miss a take if *exactly* (1<<32)-1 wakes happen
1379 // between it observing the epoch and sleeping on it, but this is extraordinarily
1380 // unlikely due to the precise number of calls required.
1381 _ = cond.epoch.fetchAdd(1, .release); // `.release` to ensure ordered after `state` update
1382 io.futexWake(u32, &cond.epoch.raw, prev_state.waiters - prev_state.signals);
1383 return;
1384 };
1385 }
13031386 }
1304
1305 pub const Wake = enum {
1306 /// Wake up only one thread.
1307 one,
1308 /// Wake up all threads.
1309 all,
1310 };
13111387};
13121388
13131389pub const TypeErasedQueue = struct {
lib/std/Io/Threaded.zig-200
......@@ -621,14 +621,6 @@ pub fn io(t: *Threaded) Io {
621621 .futexWaitUncancelable = futexWaitUncancelable,
622622 .futexWake = futexWake,
623623
624 .mutexLock = mutexLock,
625 .mutexLockUncancelable = mutexLockUncancelable,
626 .mutexUnlock = mutexUnlock,
627
628 .conditionWait = conditionWait,
629 .conditionWaitUncancelable = conditionWaitUncancelable,
630 .conditionWake = conditionWake,
631
632624 .dirMake = dirMake,
633625 .dirMakePath = dirMakePath,
634626 .dirMakeOpenPath = dirMakeOpenPath,
......@@ -721,14 +713,6 @@ pub fn ioBasic(t: *Threaded) Io {
721713 .futexWaitUncancelable = futexWaitUncancelable,
722714 .futexWake = futexWake,
723715
724 .mutexLock = mutexLock,
725 .mutexLockUncancelable = mutexLockUncancelable,
726 .mutexUnlock = mutexUnlock,
727
728 .conditionWait = conditionWait,
729 .conditionWaitUncancelable = conditionWaitUncancelable,
730 .conditionWake = conditionWake,
731
732716 .dirMake = dirMake,
733717 .dirMakePath = dirMakePath,
734718 .dirMakeOpenPath = dirMakeOpenPath,
......@@ -1274,190 +1258,6 @@ fn futexWake(userdata: ?*anyopaque, ptr: *const u32, max_waiters: u32) void {
12741258 }
12751259}
12761260
1277fn mutexLock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mutex) Io.Cancelable!void {
1278 if (builtin.single_threaded) unreachable; // Interface should have prevented this.
1279 if (native_os == .netbsd) @panic("TODO");
1280 if (native_os == .openbsd) @panic("TODO");
1281 const t: *Threaded = @ptrCast(@alignCast(userdata));
1282 const current_thread = Thread.getCurrent(t);
1283 if (prev_state == .contended) {
1284 try current_thread.futexWait(@ptrCast(&mutex.state), @intFromEnum(Io.Mutex.State.contended));
1285 }
1286 while (@atomicRmw(Io.Mutex.State, &mutex.state, .Xchg, .contended, .acquire) != .unlocked) {
1287 try current_thread.futexWait(@ptrCast(&mutex.state), @intFromEnum(Io.Mutex.State.contended));
1288 }
1289}
1290
1291fn mutexLockUncancelable(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mutex) void {
1292 if (builtin.single_threaded) unreachable; // Interface should have prevented this.
1293 if (native_os == .netbsd) @panic("TODO");
1294 if (native_os == .openbsd) @panic("TODO");
1295 _ = userdata;
1296 if (prev_state == .contended) {
1297 Thread.futexWaitUncancelable(@ptrCast(&mutex.state), @intFromEnum(Io.Mutex.State.contended));
1298 }
1299 while (@atomicRmw(Io.Mutex.State, &mutex.state, .Xchg, .contended, .acquire) != .unlocked) {
1300 Thread.futexWaitUncancelable(@ptrCast(&mutex.state), @intFromEnum(Io.Mutex.State.contended));
1301 }
1302}
1303
1304fn mutexUnlock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mutex) void {
1305 if (builtin.single_threaded) unreachable; // Interface should have prevented this.
1306 if (native_os == .netbsd) @panic("TODO");
1307 if (native_os == .openbsd) @panic("TODO");
1308 _ = userdata;
1309 _ = prev_state;
1310 if (@atomicRmw(Io.Mutex.State, &mutex.state, .Xchg, .unlocked, .release) == .contended) {
1311 Thread.futexWake(@ptrCast(&mutex.state), 1);
1312 }
1313}
1314
1315fn conditionWaitUncancelable(userdata: ?*anyopaque, cond: *Io.Condition, mutex: *Io.Mutex) void {
1316 if (builtin.single_threaded) unreachable; // Deadlock.
1317 if (native_os == .netbsd) @panic("TODO");
1318 if (native_os == .openbsd) @panic("TODO");
1319 const t: *Threaded = @ptrCast(@alignCast(userdata));
1320 const t_io = ioBasic(t);
1321 comptime assert(@TypeOf(cond.state) == u64);
1322 const ints: *[2]std.atomic.Value(u32) = @ptrCast(&cond.state);
1323 const cond_state = &ints[0];
1324 const cond_epoch = &ints[1];
1325 const one_waiter = 1;
1326 const waiter_mask = 0xffff;
1327 const one_signal = 1 << 16;
1328 const signal_mask = 0xffff << 16;
1329 var epoch = cond_epoch.load(.acquire);
1330 var state = cond_state.fetchAdd(one_waiter, .monotonic);
1331 assert(state & waiter_mask != waiter_mask);
1332 state += one_waiter;
1333
1334 mutex.unlock(t_io);
1335 defer mutex.lockUncancelable(t_io);
1336
1337 while (true) {
1338 Thread.futexWaitUncancelable(@ptrCast(cond_epoch), epoch);
1339 epoch = cond_epoch.load(.acquire);
1340 state = cond_state.load(.monotonic);
1341 while (state & signal_mask != 0) {
1342 const new_state = state - one_waiter - one_signal;
1343 state = cond_state.cmpxchgWeak(state, new_state, .acquire, .monotonic) orelse return;
1344 }
1345 }
1346}
1347
1348fn conditionWait(userdata: ?*anyopaque, cond: *Io.Condition, mutex: *Io.Mutex) Io.Cancelable!void {
1349 if (builtin.single_threaded) unreachable; // Deadlock.
1350 if (native_os == .netbsd) @panic("TODO");
1351 if (native_os == .openbsd) @panic("TODO");
1352 const t: *Threaded = @ptrCast(@alignCast(userdata));
1353 const current_thread = Thread.getCurrent(t);
1354 const t_io = ioBasic(t);
1355 comptime assert(@TypeOf(cond.state) == u64);
1356 const ints: *[2]std.atomic.Value(u32) = @ptrCast(&cond.state);
1357 const cond_state = &ints[0];
1358 const cond_epoch = &ints[1];
1359 const one_waiter = 1;
1360 const waiter_mask = 0xffff;
1361 const one_signal = 1 << 16;
1362 const signal_mask = 0xffff << 16;
1363 // Observe the epoch, then check the state again to see if we should wake up.
1364 // The epoch must be observed before we check the state or we could potentially miss a wake() and deadlock:
1365 //
1366 // - T1: s = LOAD(&state)
1367 // - T2: UPDATE(&s, signal)
1368 // - T2: UPDATE(&epoch, 1) + FUTEX_WAKE(&epoch)
1369 // - T1: e = LOAD(&epoch) (was reordered after the state load)
1370 // - T1: s & signals == 0 -> FUTEX_WAIT(&epoch, e) (missed the state update + the epoch change)
1371 //
1372 // Acquire barrier to ensure the epoch load happens before the state load.
1373 var epoch = cond_epoch.load(.acquire);
1374 var state = cond_state.fetchAdd(one_waiter, .monotonic);
1375 assert(state & waiter_mask != waiter_mask);
1376 state += one_waiter;
1377
1378 mutex.unlock(t_io);
1379 defer mutex.lockUncancelable(t_io);
1380
1381 while (true) {
1382 try current_thread.futexWait(@ptrCast(cond_epoch), epoch);
1383
1384 epoch = cond_epoch.load(.acquire);
1385 state = cond_state.load(.monotonic);
1386
1387 // Try to wake up by consuming a signal and decremented the waiter we
1388 // added previously. Acquire barrier ensures code before the wake()
1389 // which added the signal happens before we decrement it and return.
1390 while (state & signal_mask != 0) {
1391 const new_state = state - one_waiter - one_signal;
1392 state = cond_state.cmpxchgWeak(state, new_state, .acquire, .monotonic) orelse return;
1393 }
1394 }
1395}
1396
1397fn conditionWake(userdata: ?*anyopaque, cond: *Io.Condition, wake: Io.Condition.Wake) void {
1398 if (builtin.single_threaded) unreachable; // Nothing to wake up.
1399 const t: *Threaded = @ptrCast(@alignCast(userdata));
1400 _ = t;
1401 comptime assert(@TypeOf(cond.state) == u64);
1402 const ints: *[2]std.atomic.Value(u32) = @ptrCast(&cond.state);
1403 const cond_state = &ints[0];
1404 const cond_epoch = &ints[1];
1405 const one_waiter = 1;
1406 const waiter_mask = 0xffff;
1407 const one_signal = 1 << 16;
1408 const signal_mask = 0xffff << 16;
1409 var state = cond_state.load(.monotonic);
1410 while (true) {
1411 const waiters = (state & waiter_mask) / one_waiter;
1412 const signals = (state & signal_mask) / one_signal;
1413
1414 // Reserves which waiters to wake up by incrementing the signals count.
1415 // Therefore, the signals count is always less than or equal to the
1416 // waiters count. We don't need to Futex.wake if there's nothing to
1417 // wake up or if other wake() threads have reserved to wake up the
1418 // current waiters.
1419 const wakeable = waiters - signals;
1420 if (wakeable == 0) {
1421 return;
1422 }
1423
1424 const to_wake = switch (wake) {
1425 .one => 1,
1426 .all => wakeable,
1427 };
1428
1429 // Reserve the amount of waiters to wake by incrementing the signals
1430 // count. Release barrier ensures code before the wake() happens before
1431 // the signal it posted and consumed by the wait() threads.
1432 const new_state = state + (one_signal * to_wake);
1433 state = cond_state.cmpxchgWeak(state, new_state, .release, .monotonic) orelse {
1434 // Wake up the waiting threads we reserved above by changing the epoch value.
1435 //
1436 // A waiting thread could miss a wake up if *exactly* ((1<<32)-1)
1437 // wake()s happen between it observing the epoch and sleeping on
1438 // it. This is very unlikely due to how many precise amount of
1439 // Futex.wake() calls that would be between the waiting thread's
1440 // potential preemption.
1441 //
1442 // Release barrier ensures the signal being added to the state
1443 // happens before the epoch is changed. If not, the waiting thread
1444 // could potentially deadlock from missing both the state and epoch
1445 // change:
1446 //
1447 // - T2: UPDATE(&epoch, 1) (reordered before the state change)
1448 // - T1: e = LOAD(&epoch)
1449 // - T1: s = LOAD(&state)
1450 // - T2: UPDATE(&state, signal) + FUTEX_WAKE(&epoch)
1451 // - T1: s & signals == 0 -> FUTEX_WAIT(&epoch, e) (missed both epoch change and state change)
1452 _ = cond_epoch.fetchAdd(1, .release);
1453 if (native_os == .netbsd) @panic("TODO");
1454 if (native_os == .openbsd) @panic("TODO");
1455 Thread.futexWake(@ptrCast(cond_epoch), to_wake);
1456 return;
1457 };
1458 }
1459}
1460
14611261const dirMake = switch (native_os) {
14621262 .windows => dirMakeWindows,
14631263 .wasi => dirMakeWasi,