authorgravatar for lukas@lalinsky.comLukas Lalinsky <lukas@lalinsky.com> 2026-02-19 12:07:54+01:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-04-16 13:37:23-07:00
logd821446cf92b9b8974ec4a3e5d7be883d1741b4e
tree4d92045a58d4301849dca96d90006b926d92fd80
parenta8226cd536f50fd14c059812388b87bf99324d51

Implement `Condition.waitTimeout`

I'd have preferred if `vtable.futexWait` returned `error.Timeout`, since all the OS-level APIs provide it. However, if I keep the vtable untouched, I had to determine the timeout case by post-checking the deadline. It's fine functionally, but one extra syscall that be avoided at cost of changing the vtable and all the futex implementations.

2 files changed, 69 insertions(+), 9 deletions(-)

lib/std/Io.zig+25-9
...@@ -1682,19 +1682,27 @@ pub const Condition = struct {...@@ -1682,19 +1682,27 @@ pub const Condition = struct {
1682 };1682 };
16831683
1684 pub fn wait(cond: *Condition, io: Io, mutex: *Mutex) Cancelable!void {1684 pub fn wait(cond: *Condition, io: Io, mutex: *Mutex) Cancelable!void {
1685 try waitInner(cond, io, mutex, false);1685 waitInner(cond, io, mutex, .{ .timeout = .none }) catch |err| switch (err) {
1686 error.Timeout => unreachable,
1687 error.Canceled => return error.Canceled,
1688 };
1689 }
1690
1691 pub fn waitTimeout(cond: *Condition, io: Io, mutex: *Mutex, timeout: Timeout) (Cancelable || Timeout.Error)!void {
1692 return waitInner(cond, io, mutex, .{ .timeout = timeout.toDeadline(io) });
1686 }1693 }
16871694
1688 /// Same as `wait`, except does not introduce a cancelation point.1695 /// Same as `wait`, except does not introduce a cancelation point.
1689 ///1696 ///
1690 /// For a description of cancelation and cancelation points, see `Future.cancel`.1697 /// For a description of cancelation and cancelation points, see `Future.cancel`.
1691 pub fn waitUncancelable(cond: *Condition, io: Io, mutex: *Mutex) void {1698 pub fn waitUncancelable(cond: *Condition, io: Io, mutex: *Mutex) void {
1692 waitInner(cond, io, mutex, true) catch |err| switch (err) {1699 waitInner(cond, io, mutex, .uncancelable) catch |err| switch (err) {
1700 error.Timeout => unreachable,
1693 error.Canceled => unreachable,1701 error.Canceled => unreachable,
1694 };1702 };
1695 }1703 }
16961704
1697 fn waitInner(cond: *Condition, io: Io, mutex: *Mutex, uncancelable: bool) Cancelable!void {1705 fn waitInner(cond: *Condition, io: Io, mutex: *Mutex, mode: union(enum) { uncancelable, timeout: Timeout }) (Cancelable || Timeout.Error)!void {
1698 var epoch = cond.epoch.load(.acquire); // `.acquire` to ensure ordered before state load1706 var epoch = cond.epoch.load(.acquire); // `.acquire` to ensure ordered before state load
16991707
1700 {1708 {
...@@ -1706,10 +1714,10 @@ pub const Condition = struct {...@@ -1706,10 +1714,10 @@ pub const Condition = struct {
1706 defer mutex.lockUncancelable(io);1714 defer mutex.lockUncancelable(io);
17071715
1708 while (true) {1716 while (true) {
1709 const result = if (uncancelable)1717 const result = switch (mode) {
1710 io.futexWaitUncancelable(u32, &cond.epoch.raw, epoch)1718 .uncancelable => io.futexWaitUncancelable(u32, &cond.epoch.raw, epoch),
1711 else1719 .timeout => |t| io.futexWaitTimeout(u32, &cond.epoch.raw, epoch, t),
1712 io.futexWait(u32, &cond.epoch.raw, epoch);1720 };
17131721
1714 epoch = cond.epoch.load(.acquire); // `.acquire` to ensure ordered before `state` laod1722 epoch = cond.epoch.load(.acquire); // `.acquire` to ensure ordered before `state` laod
17151723
...@@ -1729,13 +1737,21 @@ pub const Condition = struct {...@@ -1729,13 +1737,21 @@ pub const Condition = struct {
1729 }1737 }
17301738
1731 // There are no more signals available; this was a spurious wakeup or an error. If it1739 // There are no more signals available; this was a spurious wakeup or an error. If it
1732 // was an error, we will remove ourselves as a waiter and return that error. Otherwise,1740 // was an error, we will remove ourselves as a waiter and return that error. If a
1733 // we'll loop back to the futex wait.1741 // timeout was specified and the deadline has passed, we remove ourselves as a waiter
1742 // and return `error.Timeout`. Otherwise, we'll loop back to the futex wait.
1734 result catch |err| {1743 result catch |err| {
1735 const prev_state = cond.state.fetchSub(.{ .waiters = 1, .signals = 0 }, .monotonic);1744 const prev_state = cond.state.fetchSub(.{ .waiters = 1, .signals = 0 }, .monotonic);
1736 assert(prev_state.waiters > 0); // underflow caused by illegal state1745 assert(prev_state.waiters > 0); // underflow caused by illegal state
1737 return err;1746 return err;
1738 };1747 };
1748 if (mode == .timeout and mode.timeout != .none) {
1749 if (mode.timeout.deadline.untilNow(io).raw.nanoseconds >= 0) {
1750 const prev_state = cond.state.fetchSub(.{ .waiters = 1, .signals = 0 }, .monotonic);
1751 assert(prev_state.waiters > 0); // underflow caused by illegal state
1752 return error.Timeout;
1753 }
1754 }
1739 }1755 }
1740 }1756 }
17411757
lib/std/Io/test.zig+44
...@@ -971,3 +971,47 @@ test "Select.cancel with no tasks, no deadlock" {...@@ -971,3 +971,47 @@ test "Select.cancel with no tasks, no deadlock" {
971 var select: Io.Select(U) = .init(io, &.{});971 var select: Io.Select(U) = .init(io, &.{});
972 try expectEqual(null, select.cancel());972 try expectEqual(null, select.cancel());
973}973}
974
975test "Condition" {
976 if (builtin.single_threaded) return error.SkipZigTest;
977 const io = testing.io;
978
979 const TestContext = struct {
980 ready: Io.Event = .unset,
981 mutex: Io.Mutex = .init,
982 cond: Io.Condition = .init,
983 value: u32 = 0,
984
985 fn worker(ctx: *@This()) !void {
986 defer ctx.ready.set(io);
987
988 try ctx.mutex.lock(io);
989 defer ctx.mutex.unlock(io);
990
991 try expectError(error.Timeout, ctx.cond.waitTimeout(io, &ctx.mutex, .{ .duration = .{
992 .raw = .fromMilliseconds(1),
993 .clock = .awake,
994 } }));
995 try expectEqual(0, ctx.value);
996
997 ctx.ready.set(io);
998
999 while (ctx.value == 0) try ctx.cond.wait(io, &ctx.mutex);
1000 try expectEqual(1, ctx.value);
1001 }
1002 };
1003
1004 var ctx: TestContext = .{};
1005
1006 var future = try io.concurrent(TestContext.worker, .{&ctx});
1007 defer future.cancel(io) catch {};
1008
1009 try ctx.ready.wait(io);
1010
1011 try ctx.mutex.lock(io);
1012 ctx.value = 1;
1013 ctx.mutex.unlock(io);
1014 ctx.cond.signal(io);
1015
1016 try future.await(io);
1017}