authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-04-16 14:51:25-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-04-16 14:52:48-07:00
logc0763b5e257ac7c0d42877d17a2af2db0910221e
tree967a7cd4940b98f10cc327067648839e1d2521ac
parent078185a54baf6093d3f43cd5b51c32322b34af75

std.Io.Condition: separate wait impls for clarity

also: * add docs * add test coverage for waitUncancelable * explicit error set declaration WaitTimeoutError

2 files changed, 103 insertions(+), 21 deletions(-)

lib/std/Io.zig+59-20
......@@ -1681,28 +1681,29 @@ pub const Condition = struct {
16811681 .epoch = .init(0),
16821682 };
16831683
1684 /// Blocks until the condition is signaled or canceled.
1685 ///
1686 /// See also:
1687 /// * `waitUncancelable`
1688 /// * `waitTimeout`
16841689 pub fn wait(cond: *Condition, io: Io, mutex: *Mutex) Cancelable!void {
1685 waitInner(cond, io, mutex, .{ .timeout = .none }) catch |err| switch (err) {
1690 waitTimeout(cond, io, mutex, .none) catch |err| switch (err) {
16861691 error.Timeout => unreachable,
1687 error.Canceled => return error.Canceled,
1692 error.Canceled => |e| return e,
16881693 };
16891694 }
16901695
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) });
1693 }
1696 pub const WaitTimeoutError = Cancelable || Timeout.Error;
16941697
1695 /// Same as `wait`, except does not introduce a cancelation point.
1698 /// Blocks until the condition is signaled, canceled, or the provided
1699 /// timeout expires.
16961700 ///
1697 /// For a description of cancelation and cancelation points, see `Future.cancel`.
1698 pub fn waitUncancelable(cond: *Condition, io: Io, mutex: *Mutex) void {
1699 waitInner(cond, io, mutex, .uncancelable) catch |err| switch (err) {
1700 error.Timeout => unreachable,
1701 error.Canceled => unreachable,
1702 };
1703 }
1701 /// See also:
1702 /// * `wait`
1703 /// * `waitUncancelable`
1704 pub fn waitTimeout(cond: *Condition, io: Io, mutex: *Mutex, timeout: Timeout) WaitTimeoutError!void {
1705 const deadline = timeout.toDeadline(io);
17041706
1705 fn waitInner(cond: *Condition, io: Io, mutex: *Mutex, mode: union(enum) { uncancelable, timeout: Timeout }) (Cancelable || Timeout.Error)!void {
17061707 var epoch = cond.epoch.load(.acquire); // `.acquire` to ensure ordered before state load
17071708
17081709 {
......@@ -1714,10 +1715,7 @@ pub const Condition = struct {
17141715 defer mutex.lockUncancelable(io);
17151716
17161717 while (true) {
1717 const result = switch (mode) {
1718 .uncancelable => io.futexWaitUncancelable(u32, &cond.epoch.raw, epoch),
1719 .timeout => |t| io.futexWaitTimeout(u32, &cond.epoch.raw, epoch, t),
1720 };
1718 const result = io.futexWaitTimeout(u32, &cond.epoch.raw, epoch, deadline);
17211719
17221720 epoch = cond.epoch.load(.acquire); // `.acquire` to ensure ordered before `state` laod
17231721
......@@ -1745,13 +1743,54 @@ pub const Condition = struct {
17451743 assert(prev_state.waiters > 0); // underflow caused by illegal state
17461744 return err;
17471745 };
1748 if (mode == .timeout and mode.timeout != .none) {
1749 if (mode.timeout.deadline.untilNow(io).raw.nanoseconds >= 0) {
1746 switch (deadline) {
1747 .none => {},
1748 .deadline => |d| if (d.untilNow(io).raw.nanoseconds >= 0) {
17501749 const prev_state = cond.state.fetchSub(.{ .waiters = 1, .signals = 0 }, .monotonic);
17511750 assert(prev_state.waiters > 0); // underflow caused by illegal state
17521751 return error.Timeout;
1752 },
1753 .duration => unreachable,
1754 }
1755 }
1756 }
1757
1758 /// Same as `wait`, except does not introduce a cancelation point.
1759 ///
1760 /// See `Future.cancel` for a description of cancelation points.
1761 pub fn waitUncancelable(cond: *Condition, io: Io, mutex: *Mutex) void {
1762 var epoch = cond.epoch.load(.acquire); // `.acquire` to ensure ordered before state load
1763
1764 {
1765 const prev_state = cond.state.fetchAdd(.{ .waiters = 1, .signals = 0 }, .monotonic);
1766 assert(prev_state.waiters < math.maxInt(u16)); // overflow caused by too many waiters
1767 }
1768
1769 mutex.unlock(io);
1770 defer mutex.lockUncancelable(io);
1771
1772 while (true) {
1773 io.futexWaitUncancelable(u32, &cond.epoch.raw, epoch);
1774
1775 epoch = cond.epoch.load(.acquire); // `.acquire` to ensure ordered before `state` laod
1776
1777 // Even on error, try to consume a pending signal first. Otherwise a race might
1778 // cause a signal to get stuck in the state with no corresponding waiter.
1779 {
1780 var prev_state = cond.state.load(.monotonic);
1781 while (prev_state.signals > 0) {
1782 prev_state = cond.state.cmpxchgWeak(prev_state, .{
1783 .waiters = prev_state.waiters - 1,
1784 .signals = prev_state.signals - 1,
1785 }, .acquire, .monotonic) orelse {
1786 // We successfully consumed a signal.
1787 return;
1788 };
17531789 }
17541790 }
1791
1792 // There are no more signals available; this was a spurious wakeup,
1793 // so we'll loop back to the futex wait.
17551794 }
17561795 }
17571796
lib/std/Io/test.zig+44-1
......@@ -972,7 +972,7 @@ test "Select.cancel with no tasks, no deadlock" {
972972 try expectEqual(null, select.cancel());
973973}
974974
975test "Condition" {
975test "Condition.waitTimeout" {
976976 const io = testing.io;
977977
978978 const Context = struct {
......@@ -1016,3 +1016,46 @@ test "Condition" {
10161016
10171017 try future.await(io);
10181018}
1019
1020test "Condition.waitUncancelable" {
1021 const io = testing.io;
1022
1023 const Context = struct {
1024 ready: Io.Event = .unset,
1025 mutex: Io.Mutex = .init,
1026 cond: Io.Condition = .init,
1027 value: u32 = 0,
1028
1029 fn worker(ctx: *@This()) !void {
1030 defer ctx.ready.set(io);
1031
1032 try ctx.mutex.lock(io);
1033 defer ctx.mutex.unlock(io);
1034
1035 try expectEqual(0, ctx.value);
1036
1037 ctx.ready.set(io);
1038
1039 ctx.cond.waitUncancelable(io, &ctx.mutex);
1040
1041 while (ctx.value == 0) try ctx.cond.wait(io, &ctx.mutex);
1042 try expectEqual(1, ctx.value);
1043 }
1044 };
1045
1046 var ctx: Context = .{};
1047
1048 var future = io.concurrent(Context.worker, .{&ctx}) catch |err| switch (err) {
1049 error.ConcurrencyUnavailable => return error.SkipZigTest,
1050 };
1051 defer future.cancel(io) catch {};
1052
1053 try ctx.ready.wait(io);
1054
1055 try ctx.mutex.lock(io);
1056 ctx.value = 1;
1057 ctx.mutex.unlock(io);
1058 ctx.cond.signal(io);
1059
1060 try future.await(io);
1061}