authorgravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2025-12-15 14:02:20+00:00
committergravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2025-12-21 13:07:03+00:00
log0649f96da3e89c1397824e3b42702024b70ec332
tree56e5ab96527aa815b4a731f71f72484ff6ea72ca
parentb4ee54b5804b8afb0387a36c913a2cd8cf595409
signaturelock-open Commit is signed but in an unrecognized format.

std.Io: introduce Event implemented using futex


2 files changed, 139 insertions(+), 0 deletions(-)

lib/std/Io.zig+103
......@@ -1386,6 +1386,109 @@ pub const Condition = struct {
13861386 }
13871387};
13881388
1389/// Logical boolean flag which can be set and unset and supports a "wait until set" operation.
1390pub const Event = enum(u32) {
1391 unset,
1392 waiting,
1393 is_set,
1394
1395 /// Returns whether the logical boolean is `true`.
1396 pub fn isSet(event: *const Event) bool {
1397 return switch (@atomicLoad(Event, event, .acquire)) {
1398 .unset, .waiting => false,
1399 .is_set => true,
1400 };
1401 }
1402
1403 /// Blocks until the logical boolean is `true`.
1404 pub fn wait(event: *Event, io: Io) Io.Cancelable!void {
1405 if (@cmpxchgStrong(Event, event, .unset, .waiting, .acquire, .acquire)) |prev| switch (prev) {
1406 .unset => unreachable,
1407 .waiting => {},
1408 .is_set => return,
1409 };
1410 errdefer {
1411 // Ideally we would restore the event back to `.unset` instead of `.waiting`, but there
1412 // might be other threads waiting on the event. In theory we could track the *number* of
1413 // waiting threads in the unused bits of the `Event`, but that has its own problem: the
1414 // waiters would wake up when a *new waiter* was added. So it's easiest to just leave
1415 // the state at `.waiting`---at worst it causes one redundant call to `futexWake`.
1416 }
1417 while (true) {
1418 try io.futexWait(Event, event, .waiting);
1419 switch (@atomicLoad(Event, event, .acquire)) {
1420 .unset => unreachable, // `reset` called before pending `wait` returned
1421 .waiting => continue,
1422 .is_set => return,
1423 }
1424 }
1425 }
1426
1427 /// Same as `wait` except uninterruptible.
1428 pub fn waitUncancelable(event: *Event, io: Io) void {
1429 if (@cmpxchgStrong(Event, event, .unset, .waiting, .acquire, .acquire)) |prev| switch (prev) {
1430 .unset => unreachable,
1431 .waiting => {},
1432 .is_set => return,
1433 };
1434 while (true) {
1435 io.futexWaitUncancelable(Event, event, .waiting);
1436 switch (@atomicLoad(Event, event, .acquire)) {
1437 .unset => unreachable, // `reset` called before pending `wait` returned
1438 .waiting => continue,
1439 .is_set => return,
1440 }
1441 }
1442 }
1443
1444 /// Blocks the calling thread until either the logical boolean is set, the timeout expires, or a
1445 /// spurious wakeup occurs. If the timeout expires or a spurious wakeup occurs, `error.Timeout`
1446 /// is returned.
1447 pub fn waitTimeout(event: *Event, io: Io, timeout: Timeout) (error{Timeout} || Cancelable)!void {
1448 if (@cmpxchgStrong(Event, event, .unset, .waiting, .acquire, .acquire)) |prev| switch (prev) {
1449 .unset => unreachable,
1450 .waiting => assert(!builtin.single_threaded), // invalid state
1451 .is_set => return,
1452 };
1453 errdefer {
1454 // Ideally we would restore the event back to `.unset` instead of `.waiting`, but there
1455 // might be other threads waiting on the event. In theory we could track the *number* of
1456 // waiting threads in the unused bits of the `Event`, but that has its own problem: the
1457 // waiters would wake up when a *new waiter* was added. So it's easiest to just leave
1458 // the state at `.waiting`---at worst it causes one redundant call to `futexWake`.
1459 }
1460 io.futexWaitTimeout(Event, event, .waiting, timeout);
1461 switch (@atomicLoad(Event, event, .acquire)) {
1462 .unset => unreachable, // `reset` called before pending `wait` returned
1463 .waiting => return error.Timeout,
1464 .is_set => return,
1465 }
1466 }
1467
1468 /// Sets the logical boolean to true, and hence unblocks any pending calls to `wait`. The
1469 /// logical boolean remains true until `reset` is called, so future calls to `set` have no
1470 /// semantic effect.
1471 ///
1472 /// Any memory accesses prior to a `set` call are "released", so that if this `set` call causes
1473 /// `isSet` to return `true` or a wait to finish, those tasks will be able to observe those
1474 /// memory accesses.
1475 pub fn set(e: *Event, io: Io) void {
1476 switch (@atomicRmw(Event, e, .Xchg, .is_set, .release)) {
1477 .unset, .is_set => {},
1478 .waiting => io.futexWake(Event, e, std.math.maxInt(u32)),
1479 }
1480 }
1481
1482 /// Sets the logical boolean to false.
1483 ///
1484 /// Assumes that there is no pending call to `wait` or `waitUncancelable`.
1485 ///
1486 /// However, concurrent calls to `isSet`, `set`, and `reset` are allowed.
1487 pub fn reset(e: *Event) void {
1488 @atomicStore(Event, e, .unset, .monotonic);
1489 }
1490};
1491
13891492pub const TypeErasedQueue = struct {
13901493 mutex: Mutex,
13911494
lib/std/Io/test.zig+36
......@@ -255,3 +255,39 @@ test "Queue" {
255255 try testQueue(4);
256256 try testQueue(5);
257257}
258
259test "Event" {
260 const global = struct {
261 fn waitAndRead(io: Io, event: *Io.Event, ptr: *const u32) Io.Cancelable!u32 {
262 try event.wait(io);
263 return ptr.*;
264 }
265 };
266
267 const io = std.testing.io;
268
269 var event: Io.Event = .unset;
270 var buffer: u32 = undefined;
271
272 {
273 var future = io.concurrent(global.waitAndRead, .{ io, &event, &buffer }) catch |err| switch (err) {
274 error.ConcurrencyUnavailable => return error.SkipZigTest,
275 };
276
277 buffer = 123;
278 event.set(io);
279
280 const result = try future.await(io);
281
282 try std.testing.expectEqual(123, result);
283 }
284
285 event.reset();
286
287 {
288 var future = io.concurrent(global.waitAndRead, .{ io, &event, &buffer }) catch |err| switch (err) {
289 error.ConcurrencyUnavailable => return error.SkipZigTest,
290 };
291 try std.testing.expectError(error.Canceled, future.cancel(io));
292 }
293}