authorgravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2025-12-20 14:46:29+00:00
committergravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2025-12-21 13:07:04+00:00
logfa7e818e144f2fd316fc54492f4699f6e1524738
tree9694e904d8c79ed9fe3d5ffda98b7c41b2f1b022
parent9bf65f6e05467496780ede466cebc9ed8a0e17f3
signaturelock-open Commit is signed but in an unrecognized format.

std.Io: add new cancelation APIs

Also, better document how cancelation actually works.

3 files changed, 251 insertions(+), 13 deletions(-)

lib/std/Io.zig+103-11
...@@ -650,6 +650,10 @@ pub const VTable = struct {...@@ -650,6 +650,10 @@ pub const VTable = struct {
650 groupWait: *const fn (?*anyopaque, *Group, token: *anyopaque) void,650 groupWait: *const fn (?*anyopaque, *Group, token: *anyopaque) void,
651 groupCancel: *const fn (?*anyopaque, *Group, token: *anyopaque) void,651 groupCancel: *const fn (?*anyopaque, *Group, token: *anyopaque) void,
652652
653 recancel: *const fn (?*anyopaque) void,
654 swapCancelProtection: *const fn (?*anyopaque, new: CancelProtection) CancelProtection,
655 checkCancel: *const fn (?*anyopaque) Cancelable!void,
656
653 /// Blocks until one of the futures from the list has a result ready, such657 /// Blocks until one of the futures from the list has a result ready, such
654 /// that awaiting it will not block. Returns that index.658 /// that awaiting it will not block. Returns that index.
655 select: *const fn (?*anyopaque, futures: []const *AnyFuture) Cancelable!usize,659 select: *const fn (?*anyopaque, futures: []const *AnyFuture) Cancelable!usize,
...@@ -982,7 +986,14 @@ pub fn Future(Result: type) type {...@@ -982,7 +986,14 @@ pub fn Future(Result: type) type {
982 any_future: ?*AnyFuture,986 any_future: ?*AnyFuture,
983 result: Result,987 result: Result,
984988
985 /// Equivalent to `await` but places a cancellation request.989 /// Equivalent to `await` but places a cancellation request. This causes the task to receive
990 /// `error.Canceled` from its next "cancelation point" (if any). A cancelation point is a
991 /// call to a function in `Io` which can return `error.Canceled`.
992 ///
993 /// After cancelation of a task is requested, only the next cancelation point in that task
994 /// will return `error.Canceled`: future points will not re-signal the cancelation. As such,
995 /// it is usually a bug to ignore `error.Canceled`. However, to defer handling cancelation
996 /// requests, see also `recancel` and `CancelProtection`.
986 ///997 ///
987 /// Idempotent. Not threadsafe.998 /// Idempotent. Not threadsafe.
988 pub fn cancel(f: *@This(), io: Io) Result {999 pub fn cancel(f: *@This(), io: Io) Result {
...@@ -1079,6 +1090,8 @@ pub const Group = struct {...@@ -1079,6 +1090,8 @@ pub const Group = struct {
1079 /// Equivalent to `wait` but immediately requests cancellation on all1090 /// Equivalent to `wait` but immediately requests cancellation on all
1080 /// members of the group.1091 /// members of the group.
1081 ///1092 ///
1093 /// For a description of cancelation and cancelation points, see `Future.cancel`.
1094 ///
1082 /// Idempotent. Not threadsafe.1095 /// Idempotent. Not threadsafe.
1083 pub fn cancel(g: *Group, io: Io) void {1096 pub fn cancel(g: *Group, io: Io) void {
1084 const token = g.token orelse return;1097 const token = g.token orelse return;
...@@ -1087,6 +1100,61 @@ pub const Group = struct {...@@ -1087,6 +1100,61 @@ pub const Group = struct {
1087 }1100 }
1088};1101};
10891102
1103/// Asserts that `error.Canceled` was returned from a prior cancelation point, and "re-arms" the
1104/// cancelation request, so that `error.Canceled` will be returned again from the next cancelation
1105/// point.
1106///
1107/// For a description of cancelation and cancelation points, see `Future.cancel`.
1108pub fn recancel(io: Io) void {
1109 io.vtable.recancel(io.userdata);
1110}
1111
1112/// In rare cases, it is desirable to completely block cancelation notification, so that a region
1113/// of code can run uninterrupted before `error.Canceled` is potentially observed. Therefore, every
1114/// task has a "cancel protection" state which indicates whether or not `Io` functions can introduce
1115/// cancelation points.
1116///
1117/// To modify a task's cancel protection state, see `swapCancelProtection`.
1118///
1119/// For a description of cancelation and cancelation points, see `Future.cancel`.
1120pub const CancelProtection = enum {
1121 /// Any call to an `Io` function with `error.Canceled` in its error set is a cancelation point.
1122 ///
1123 /// This is the default state, which all tasks are created in.
1124 unblocked,
1125 /// No `Io` function introduces a cancelation point (`error.Canceled` will never be returned).
1126 blocked,
1127};
1128/// Updates the current task's cancel protection state (see `CancelProtection`).
1129///
1130/// The typical usage for this function is to protect a block of code from cancelation:
1131/// ```
1132/// const old_cancel_protect = io.swapCancelProtection(.blocked);
1133/// defer _ = io.swapCancelProtection(old_cancel_protect);
1134/// doSomeWork() catch |err| switch (err) {
1135/// error.Canceled => unreachable,
1136/// };
1137/// ```
1138///
1139/// For a description of cancelation and cancelation points, see `Future.cancel`.
1140pub fn swapCancelProtection(io: Io, new: CancelProtection) CancelProtection {
1141 return io.vtable.swapCancelProtection(io.userdata, new);
1142}
1143
1144/// This function acts as a pure cancelation point (subject to protection; see `CancelProtection`)
1145/// and does nothing else. In other words, it returns `error.Canceled` if there is an outstanding
1146/// non-blocked cancelation request, but otherwise is a no-op.
1147///
1148/// It is rarely necessary to call this function. The primary use case is in long-running CPU-bound
1149/// tasks which may need to respond to cancelation before completing. Short tasks, or those which
1150/// perform other `Io` operations (and hence have other cancelation points), will typically already
1151/// respond quickly to cancelation requests.
1152///
1153/// For a description of cancelation and cancelation points, see `Future.cancel`.
1154pub fn checkCancel(io: Io) Cancelable!void {
1155 return io.vtable.checkCancel(io.userdata);
1156}
1157
1090pub fn Select(comptime U: type) type {1158pub fn Select(comptime U: type) type {
1091 return struct {1159 return struct {
1092 io: Io,1160 io: Io,
...@@ -1160,6 +1228,8 @@ pub fn Select(comptime U: type) type {...@@ -1160,6 +1228,8 @@ pub fn Select(comptime U: type) type {
1160 /// Equivalent to `wait` but requests cancellation on all remaining1228 /// Equivalent to `wait` but requests cancellation on all remaining
1161 /// tasks owned by the select.1229 /// tasks owned by the select.
1162 ///1230 ///
1231 /// For a description of cancelation and cancelation points, see `Future.cancel`.
1232 ///
1163 /// It is illegal to call `wait` after this.1233 /// It is illegal to call `wait` after this.
1164 ///1234 ///
1165 /// Idempotent. Not threadsafe.1235 /// Idempotent. Not threadsafe.
...@@ -1193,7 +1263,9 @@ pub fn futexWaitTimeout(io: Io, comptime T: type, ptr: *align(@alignOf(u32)) con...@@ -1193,7 +1263,9 @@ pub fn futexWaitTimeout(io: Io, comptime T: type, ptr: *align(@alignOf(u32)) con
1193 const expected_raw: *align(1) const u32 = @ptrCast(&expected);1263 const expected_raw: *align(1) const u32 = @ptrCast(&expected);
1194 return io.vtable.futexWait(io.userdata, @ptrCast(ptr), expected_raw.*, timeout);1264 return io.vtable.futexWait(io.userdata, @ptrCast(ptr), expected_raw.*, timeout);
1195}1265}
1196/// Same as `futexWait`, except is not affected by task cancelation.1266/// Same as `futexWait`, except does not introduce a cancelation point.
1267///
1268/// For a description of cancelation and cancelation points, see `Future.cancel`.
1197pub fn futexWaitUncancelable(io: Io, comptime T: type, ptr: *align(@alignOf(u32)) const T, expected: T) void {1269pub fn futexWaitUncancelable(io: Io, comptime T: type, ptr: *align(@alignOf(u32)) const T, expected: T) void {
1198 comptime assert(@sizeOf(T) == @sizeOf(u32));1270 comptime assert(@sizeOf(T) == @sizeOf(u32));
1199 const expected_raw: *align(1) const u32 = @ptrCast(&expected);1271 const expected_raw: *align(1) const u32 = @ptrCast(&expected);
...@@ -1247,6 +1319,9 @@ pub const Mutex = struct {...@@ -1247,6 +1319,9 @@ pub const Mutex = struct {
1247 }1319 }
1248 }1320 }
12491321
1322 /// Same as `lock`, except does not introduce a cancelation point.
1323 ///
1324 /// For a description of cancelation and cancelation points, see `Future.cancel`.
1250 pub fn lockUncancelable(m: *Mutex, io: Io) void {1325 pub fn lockUncancelable(m: *Mutex, io: Io) void {
1251 const initial_state = m.state.cmpxchgWeak(1326 const initial_state = m.state.cmpxchgWeak(
1252 .unlocked,1327 .unlocked,
...@@ -1296,6 +1371,9 @@ pub const Condition = struct {...@@ -1296,6 +1371,9 @@ pub const Condition = struct {
1296 try waitInner(cond, io, mutex, false);1371 try waitInner(cond, io, mutex, false);
1297 }1372 }
12981373
1374 /// Same as `wait`, except does not introduce a cancelation point.
1375 ///
1376 /// For a description of cancelation and cancelation points, see `Future.cancel`.
1299 pub fn waitUncancelable(cond: *Condition, io: Io, mutex: *Mutex) void {1377 pub fn waitUncancelable(cond: *Condition, io: Io, mutex: *Mutex) void {
1300 waitInner(cond, io, mutex, true) catch |err| switch (err) {1378 waitInner(cond, io, mutex, true) catch |err| switch (err) {
1301 error.Canceled => unreachable,1379 error.Canceled => unreachable,
...@@ -1424,7 +1502,9 @@ pub const Event = enum(u32) {...@@ -1424,7 +1502,9 @@ pub const Event = enum(u32) {
1424 }1502 }
1425 }1503 }
14261504
1427 /// Same as `wait` except uninterruptible.1505 /// Same as `wait`, except does not introduce a cancelation point.
1506 ///
1507 /// For a description of cancelation and cancelation points, see `Future.cancel`.
1428 pub fn waitUncancelable(event: *Event, io: Io) void {1508 pub fn waitUncancelable(event: *Event, io: Io) void {
1429 if (@cmpxchgStrong(Event, event, .unset, .waiting, .acquire, .acquire)) |prev| switch (prev) {1509 if (@cmpxchgStrong(Event, event, .unset, .waiting, .acquire, .acquire)) |prev| switch (prev) {
1430 .unset => unreachable,1510 .unset => unreachable,
...@@ -1531,7 +1611,9 @@ pub const TypeErasedQueue = struct {...@@ -1531,7 +1611,9 @@ pub const TypeErasedQueue = struct {
1531 return q.putLocked(io, elements, min, false);1611 return q.putLocked(io, elements, min, false);
1532 }1612 }
15331613
1534 /// Same as `put` but cannot be canceled.1614 /// Same as `put`, except does not introduce a cancelation point.
1615 ///
1616 /// For a description of cancelation and cancelation points, see `Future.cancel`.
1535 pub fn putUncancelable(q: *TypeErasedQueue, io: Io, elements: []const u8, min: usize) usize {1617 pub fn putUncancelable(q: *TypeErasedQueue, io: Io, elements: []const u8, min: usize) usize {
1536 assert(elements.len >= min);1618 assert(elements.len >= min);
1537 if (elements.len == 0) return 0;1619 if (elements.len == 0) return 0;
...@@ -1602,7 +1684,10 @@ pub const TypeErasedQueue = struct {...@@ -1602,7 +1684,10 @@ pub const TypeErasedQueue = struct {
1602 return q.getLocked(io, buffer, min, false);1684 return q.getLocked(io, buffer, min, false);
1603 }1685 }
16041686
1605 pub fn getUncancelable(q: *@This(), io: Io, buffer: []u8, min: usize) usize {1687 /// Same as `get`, except does not introduce a cancelation point.
1688 ///
1689 /// For a description of cancelation and cancelation points, see `Future.cancel`.
1690 pub fn getUncancelable(q: *TypeErasedQueue, io: Io, buffer: []u8, min: usize) usize {
1606 assert(buffer.len >= min);1691 assert(buffer.len >= min);
1607 if (buffer.len == 0) return 0;1692 if (buffer.len == 0) return 0;
1608 q.mutex.lockUncancelable(io);1693 q.mutex.lockUncancelable(io);
...@@ -1722,7 +1807,9 @@ pub fn Queue(Elem: type) type {...@@ -1722,7 +1807,9 @@ pub fn Queue(Elem: type) type {
1722 assert(try q.put(io, elements, elements.len) == elements.len);1807 assert(try q.put(io, elements, elements.len) == elements.len);
1723 }1808 }
17241809
1725 /// Same as `put` but cannot be interrupted.1810 /// Same as `put`, except does not introduce a cancelation point.
1811 ///
1812 /// For a description of cancelation and cancelation points, see `Future.cancel`.
1726 pub fn putUncancelable(q: *@This(), io: Io, elements: []const Elem, min: usize) usize {1813 pub fn putUncancelable(q: *@This(), io: Io, elements: []const Elem, min: usize) usize {
1727 return @divExact(q.type_erased.putUncancelable(io, @ptrCast(elements), min * @sizeOf(Elem)), @sizeOf(Elem));1814 return @divExact(q.type_erased.putUncancelable(io, @ptrCast(elements), min * @sizeOf(Elem)), @sizeOf(Elem));
1728 }1815 }
...@@ -1731,6 +1818,9 @@ pub fn Queue(Elem: type) type {...@@ -1731,6 +1818,9 @@ pub fn Queue(Elem: type) type {
1731 assert(try q.put(io, &.{item}, 1) == 1);1818 assert(try q.put(io, &.{item}, 1) == 1);
1732 }1819 }
17331820
1821 /// Same as `putOne`, except does not introduce a cancelation point.
1822 ///
1823 /// For a description of cancelation and cancelation points, see `Future.cancel`.
1734 pub fn putOneUncancelable(q: *@This(), io: Io, item: Elem) void {1824 pub fn putOneUncancelable(q: *@This(), io: Io, item: Elem) void {
1735 assert(q.putUncancelable(io, &.{item}, 1) == 1);1825 assert(q.putUncancelable(io, &.{item}, 1) == 1);
1736 }1826 }
...@@ -1746,8 +1836,11 @@ pub fn Queue(Elem: type) type {...@@ -1746,8 +1836,11 @@ pub fn Queue(Elem: type) type {
1746 return @divExact(try q.type_erased.get(io, @ptrCast(buffer), min * @sizeOf(Elem)), @sizeOf(Elem));1836 return @divExact(try q.type_erased.get(io, @ptrCast(buffer), min * @sizeOf(Elem)), @sizeOf(Elem));
1747 }1837 }
17481838
1839 /// Same as `get`, except does not introduce a cancelation point.
1840 ///
1841 /// For a description of cancelation and cancelation points, see `Future.cancel`.
1749 pub fn getUncancelable(q: *@This(), io: Io, buffer: []Elem, min: usize) usize {1842 pub fn getUncancelable(q: *@This(), io: Io, buffer: []Elem, min: usize) usize {
1750 return @divExact(q.type_erased.getUncancelable(io, @ptrCast(buffer), min * @sizeOf(Elem)), @sizeOf(Elem));1843 return @divExact(try q.type_erased.getUncancelable(io, @ptrCast(buffer), min * @sizeOf(Elem)), @sizeOf(Elem));
1751 }1844 }
17521845
1753 pub fn getOne(q: *@This(), io: Io) Cancelable!Elem {1846 pub fn getOne(q: *@This(), io: Io) Cancelable!Elem {
...@@ -1756,6 +1849,9 @@ pub fn Queue(Elem: type) type {...@@ -1756,6 +1849,9 @@ pub fn Queue(Elem: type) type {
1756 return buf[0];1849 return buf[0];
1757 }1850 }
17581851
1852 /// Same as `getOne`, except does not introduce a cancelation point.
1853 ///
1854 /// For a description of cancelation and cancelation points, see `Future.cancel`.
1759 pub fn getOneUncancelable(q: *@This(), io: Io) Elem {1855 pub fn getOneUncancelable(q: *@This(), io: Io) Elem {
1760 var buf: [1]Elem = undefined;1856 var buf: [1]Elem = undefined;
1761 assert(q.getUncancelable(io, &buf, 1) == 1);1857 assert(q.getUncancelable(io, &buf, 1) == 1);
...@@ -1846,10 +1942,6 @@ pub fn concurrent(...@@ -1846,10 +1942,6 @@ pub fn concurrent(
1846 return future;1942 return future;
1847}1943}
18481944
1849pub fn cancelRequested(io: Io) bool {
1850 return io.vtable.cancelRequested(io.userdata);
1851}
1852
1853pub const SleepError = error{UnsupportedClock} || UnexpectedError || Cancelable;1945pub const SleepError = error{UnsupportedClock} || UnexpectedError || Cancelable;
18541946
1855pub fn sleep(io: Io, duration: Duration, clock: Clock) SleepError!void {1947pub fn sleep(io: Io, duration: Duration, clock: Clock) SleepError!void {
lib/std/Io/Threaded.zig+73-2
...@@ -86,7 +86,9 @@ const Thread = struct {...@@ -86,7 +86,9 @@ const Thread = struct {
86 /// The value that needs to be passed to pthread_kill or tgkill in order to86 /// The value that needs to be passed to pthread_kill or tgkill in order to
87 /// send a signal.87 /// send a signal.
88 signal_id: SignaleeId,88 signal_id: SignaleeId,
89 current_closure: ?*Closure = null,89 current_closure: ?*Closure,
90 /// Only populated if `current_closure != null`. Indicates the current cancel protection mode.
91 cancel_protection: Io.CancelProtection,
9092
91 const SignaleeId = if (std.Thread.use_pthreads) std.c.pthread_t else std.Thread.Id;93 const SignaleeId = if (std.Thread.use_pthreads) std.c.pthread_t else std.Thread.Id;
9294
...@@ -98,6 +100,12 @@ const Thread = struct {...@@ -98,6 +100,12 @@ const Thread = struct {
98100
99 fn checkCancel(thread: *Thread) error{Canceled}!void {101 fn checkCancel(thread: *Thread) error{Canceled}!void {
100 const closure = thread.current_closure orelse return;102 const closure = thread.current_closure orelse return;
103
104 switch (thread.cancel_protection) {
105 .unblocked => {},
106 .blocked => return,
107 }
108
101 switch (@cmpxchgStrong(109 switch (@cmpxchgStrong(
102 CancelStatus,110 CancelStatus,
103 &closure.cancel_status,111 &closure.cancel_status,
...@@ -115,6 +123,11 @@ const Thread = struct {...@@ -115,6 +123,11 @@ const Thread = struct {
115 fn beginSyscall(thread: *Thread) error{Canceled}!void {123 fn beginSyscall(thread: *Thread) error{Canceled}!void {
116 const closure = thread.current_closure orelse return;124 const closure = thread.current_closure orelse return;
117125
126 switch (thread.cancel_protection) {
127 .unblocked => {},
128 .blocked => return,
129 }
130
118 switch (@cmpxchgStrong(131 switch (@cmpxchgStrong(
119 CancelStatus,132 CancelStatus,
120 &closure.cancel_status,133 &closure.cancel_status,
...@@ -135,6 +148,12 @@ const Thread = struct {...@@ -135,6 +148,12 @@ const Thread = struct {
135148
136 fn endSyscall(thread: *Thread) void {149 fn endSyscall(thread: *Thread) void {
137 const closure = thread.current_closure orelse return;150 const closure = thread.current_closure orelse return;
151
152 switch (thread.cancel_protection) {
153 .unblocked => {},
154 .blocked => return,
155 }
156
138 _ = @cmpxchgStrong(157 _ = @cmpxchgStrong(
139 CancelStatus,158 CancelStatus,
140 &closure.cancel_status,159 &closure.cancel_status,
...@@ -512,6 +531,8 @@ pub fn init(...@@ -512,6 +531,8 @@ pub fn init(
512 .have_signal_handler = false,531 .have_signal_handler = false,
513 .main_thread = .{532 .main_thread = .{
514 .signal_id = Thread.currentSignalId(),533 .signal_id = Thread.currentSignalId(),
534 .current_closure = null,
535 .cancel_protection = undefined,
515 },536 },
516 };537 };
517538
...@@ -546,7 +567,11 @@ pub const init_single_threaded: Threaded = .{...@@ -546,7 +567,11 @@ pub const init_single_threaded: Threaded = .{
546 .old_sig_io = undefined,567 .old_sig_io = undefined,
547 .old_sig_pipe = undefined,568 .old_sig_pipe = undefined,
548 .have_signal_handler = false,569 .have_signal_handler = false,
549 .main_thread = .{ .signal_id = undefined },570 .main_thread = .{
571 .signal_id = undefined,
572 .current_closure = null,
573 .cancel_protection = undefined,
574 },
550};575};
551576
552pub fn setAsyncLimit(t: *Threaded, new_limit: Io.Limit) void {577pub fn setAsyncLimit(t: *Threaded, new_limit: Io.Limit) void {
...@@ -581,6 +606,8 @@ fn join(t: *Threaded) void {...@@ -581,6 +606,8 @@ fn join(t: *Threaded) void {
581fn worker(t: *Threaded) void {606fn worker(t: *Threaded) void {
582 var thread: Thread = .{607 var thread: Thread = .{
583 .signal_id = Thread.currentSignalId(),608 .signal_id = Thread.currentSignalId(),
609 .current_closure = null,
610 .cancel_protection = undefined,
584 };611 };
585 Thread.current = &thread;612 Thread.current = &thread;
586613
...@@ -617,6 +644,10 @@ pub fn io(t: *Threaded) Io {...@@ -617,6 +644,10 @@ pub fn io(t: *Threaded) Io {
617 .groupWait = groupWait,644 .groupWait = groupWait,
618 .groupCancel = groupCancel,645 .groupCancel = groupCancel,
619646
647 .recancel = recancel,
648 .swapCancelProtection = swapCancelProtection,
649 .checkCancel = checkCancel,
650
620 .futexWait = futexWait,651 .futexWait = futexWait,
621 .futexWaitUncancelable = futexWaitUncancelable,652 .futexWaitUncancelable = futexWaitUncancelable,
622 .futexWake = futexWake,653 .futexWake = futexWake,
...@@ -709,6 +740,10 @@ pub fn ioBasic(t: *Threaded) Io {...@@ -709,6 +740,10 @@ pub fn ioBasic(t: *Threaded) Io {
709 .groupWait = groupWait,740 .groupWait = groupWait,
710 .groupCancel = groupCancel,741 .groupCancel = groupCancel,
711742
743 .recancel = recancel,
744 .swapCancelProtection = swapCancelProtection,
745 .checkCancel = checkCancel,
746
712 .futexWait = futexWait,747 .futexWait = futexWait,
713 .futexWaitUncancelable = futexWaitUncancelable,748 .futexWaitUncancelable = futexWaitUncancelable,
714 .futexWake = futexWake,749 .futexWake = futexWake,
...@@ -794,9 +829,14 @@ const AsyncClosure = struct {...@@ -794,9 +829,14 @@ const AsyncClosure = struct {
794 fn start(closure: *Closure, t: *Threaded) void {829 fn start(closure: *Closure, t: *Threaded) void {
795 const ac: *AsyncClosure = @alignCast(@fieldParentPtr("closure", closure));830 const ac: *AsyncClosure = @alignCast(@fieldParentPtr("closure", closure));
796 const current_thread = Thread.getCurrent(t);831 const current_thread = Thread.getCurrent(t);
832
797 current_thread.current_closure = closure;833 current_thread.current_closure = closure;
834 current_thread.cancel_protection = .unblocked;
835
798 ac.func(ac.contextPointer(), ac.resultPointer());836 ac.func(ac.contextPointer(), ac.resultPointer());
837
799 current_thread.current_closure = null;838 current_thread.current_closure = null;
839 current_thread.cancel_protection = undefined;
800840
801 if (@atomicRmw(?*Io.Event, &ac.select_condition, .Xchg, done_event, .release)) |select_event| {841 if (@atomicRmw(?*Io.Event, &ac.select_condition, .Xchg, done_event, .release)) |select_event| {
802 assert(select_event != done_event);842 assert(select_event != done_event);
...@@ -978,9 +1018,14 @@ const GroupClosure = struct {...@@ -978,9 +1018,14 @@ const GroupClosure = struct {
978 const group = gc.group;1018 const group = gc.group;
979 const group_state: *std.atomic.Value(usize) = @ptrCast(&group.state);1019 const group_state: *std.atomic.Value(usize) = @ptrCast(&group.state);
980 const event: *Io.Event = @ptrCast(&group.context);1020 const event: *Io.Event = @ptrCast(&group.context);
1021
981 current_thread.current_closure = closure;1022 current_thread.current_closure = closure;
1023 current_thread.cancel_protection = .unblocked;
1024
982 gc.func(group, gc.contextPointer());1025 gc.func(group, gc.contextPointer());
1026
983 current_thread.current_closure = null;1027 current_thread.current_closure = null;
1028 current_thread.cancel_protection = undefined;
9841029
985 const prev_state = group_state.fetchSub(sync_one_pending, .acq_rel);1030 const prev_state = group_state.fetchSub(sync_one_pending, .acq_rel);
986 assert((prev_state / sync_one_pending) > 0);1031 assert((prev_state / sync_one_pending) > 0);
...@@ -1201,6 +1246,32 @@ fn groupCancel(userdata: ?*anyopaque, group: *Io.Group, token: *anyopaque) void...@@ -1201,6 +1246,32 @@ fn groupCancel(userdata: ?*anyopaque, group: *Io.Group, token: *anyopaque) void
1201 }1246 }
1202}1247}
12031248
1249fn recancel(userdata: ?*anyopaque) void {
1250 const t: *Threaded = @ptrCast(@alignCast(userdata));
1251 const current_thread: *Thread = .getCurrent(t);
1252 const cancel_status = &current_thread.current_closure.?.cancel_status;
1253 switch (@atomicLoad(CancelStatus, cancel_status, .monotonic)) {
1254 .none => unreachable, // called `recancel` when not canceled
1255 .requested => unreachable, // called `recancel` when cancelation was already outstanding
1256 .acknowledged => {},
1257 _ => unreachable, // invalid state: not in a syscall
1258 }
1259 @atomicStore(CancelStatus, cancel_status, .requested, .monotonic);
1260}
1261
1262fn swapCancelProtection(userdata: ?*anyopaque, new: Io.CancelProtection) Io.CancelProtection {
1263 const t: *Threaded = @ptrCast(@alignCast(userdata));
1264 const current_thread: *Thread = .getCurrent(t);
1265 const old = current_thread.cancel_protection;
1266 current_thread.cancel_protection = new;
1267 return old;
1268}
1269
1270fn checkCancel(userdata: ?*anyopaque) Io.Cancelable!void {
1271 const t: *Threaded = @ptrCast(@alignCast(userdata));
1272 return Thread.getCurrent(t).checkCancel();
1273}
1274
1204fn await(1275fn await(
1205 userdata: ?*anyopaque,1276 userdata: ?*anyopaque,
1206 any_future: *Io.AnyFuture,1277 any_future: *Io.AnyFuture,
lib/std/Io/test.zig+75
...@@ -291,3 +291,78 @@ test "Event" {...@@ -291,3 +291,78 @@ test "Event" {
291 try std.testing.expectError(error.Canceled, future.cancel(io));291 try std.testing.expectError(error.Canceled, future.cancel(io));
292 }292 }
293}293}
294
295test "recancel" {
296 const global = struct {
297 fn worker(io: Io) Io.Cancelable!void {
298 var dummy_event: Io.Event = .unset;
299
300 if (dummy_event.wait(io)) {
301 return;
302 } else |err| switch (err) {
303 error.Canceled => io.recancel(),
304 }
305
306 // Now we expect to see `error.Canceled` again.
307 return dummy_event.wait(io);
308 }
309 };
310
311 const io = std.testing.io;
312 var future = io.concurrent(global.worker, .{io}) catch |err| switch (err) {
313 error.ConcurrencyUnavailable => return error.SkipZigTest,
314 };
315 if (future.cancel(io)) {
316 return error.UnexpectedSuccess; // both `wait` calls should have returned `error.Canceled`
317 } else |err| switch (err) {
318 error.Canceled => {},
319 }
320}
321
322test "swapCancelProtection" {
323 const global = struct {
324 fn waitTwice(
325 io: Io,
326 event: *Io.Event,
327 ) error{ Canceled, CanceledWhileProtected }!void {
328 // Wait for `event` while protected from cancelation.
329 {
330 const old_prot = io.swapCancelProtection(.blocked);
331 defer _ = io.swapCancelProtection(old_prot);
332 event.wait(io) catch |err| switch (err) {
333 error.Canceled => return error.CanceledWhileProtected,
334 };
335 }
336 // Reset the event (it will never be set again), and this time wait for it without protection.
337 event.reset();
338 _ = try event.wait(io);
339 }
340 fn sleepThenSet(io: Io, event: *Io.Event) !void {
341 // Give `waitTwice` a chance to get canceled.
342 try io.sleep(.fromMilliseconds(200), .awake);
343 event.set(io);
344 }
345 };
346
347 const io = std.testing.io;
348
349 var event: Io.Event = .unset;
350
351 var wait_future = io.concurrent(global.waitTwice, .{ io, &event }) catch |err| switch (err) {
352 error.ConcurrencyUnavailable => return error.SkipZigTest,
353 };
354 defer wait_future.cancel(io) catch {};
355
356 var set_future = try io.concurrent(global.sleepThenSet, .{ io, &event });
357 defer set_future.cancel(io) catch {};
358
359 if (wait_future.cancel(io)) {
360 return error.UnexpectedSuccess; // there was no `set` call to unblock the second `wait`
361 } else |err| switch (err) {
362 error.Canceled => {},
363 error.CanceledWhileProtected => |e| return e,
364 }
365
366 // Because it reached the `set`, it should be too late for `sleepThenSet` to see `error.Canceled`.
367 try set_future.cancel(io);
368}