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 {
650650 groupWait: *const fn (?*anyopaque, *Group, token: *anyopaque) void,
651651 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
653657 /// Blocks until one of the futures from the list has a result ready, such
654658 /// that awaiting it will not block. Returns that index.
655659 select: *const fn (?*anyopaque, futures: []const *AnyFuture) Cancelable!usize,
......@@ -982,7 +986,14 @@ pub fn Future(Result: type) type {
982986 any_future: ?*AnyFuture,
983987 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`.
986997 ///
987998 /// Idempotent. Not threadsafe.
988999 pub fn cancel(f: *@This(), io: Io) Result {
......@@ -1079,6 +1090,8 @@ pub const Group = struct {
10791090 /// Equivalent to `wait` but immediately requests cancellation on all
10801091 /// members of the group.
10811092 ///
1093 /// For a description of cancelation and cancelation points, see `Future.cancel`.
1094 ///
10821095 /// Idempotent. Not threadsafe.
10831096 pub fn cancel(g: *Group, io: Io) void {
10841097 const token = g.token orelse return;
......@@ -1087,6 +1100,61 @@ pub const Group = struct {
10871100 }
10881101};
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
10901158pub fn Select(comptime U: type) type {
10911159 return struct {
10921160 io: Io,
......@@ -1160,6 +1228,8 @@ pub fn Select(comptime U: type) type {
11601228 /// Equivalent to `wait` but requests cancellation on all remaining
11611229 /// tasks owned by the select.
11621230 ///
1231 /// For a description of cancelation and cancelation points, see `Future.cancel`.
1232 ///
11631233 /// It is illegal to call `wait` after this.
11641234 ///
11651235 /// Idempotent. Not threadsafe.
......@@ -1193,7 +1263,9 @@ pub fn futexWaitTimeout(io: Io, comptime T: type, ptr: *align(@alignOf(u32)) con
11931263 const expected_raw: *align(1) const u32 = @ptrCast(&expected);
11941264 return io.vtable.futexWait(io.userdata, @ptrCast(ptr), expected_raw.*, timeout);
11951265}
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`.
11971269pub fn futexWaitUncancelable(io: Io, comptime T: type, ptr: *align(@alignOf(u32)) const T, expected: T) void {
11981270 comptime assert(@sizeOf(T) == @sizeOf(u32));
11991271 const expected_raw: *align(1) const u32 = @ptrCast(&expected);
......@@ -1247,6 +1319,9 @@ pub const Mutex = struct {
12471319 }
12481320 }
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`.
12501325 pub fn lockUncancelable(m: *Mutex, io: Io) void {
12511326 const initial_state = m.state.cmpxchgWeak(
12521327 .unlocked,
......@@ -1296,6 +1371,9 @@ pub const Condition = struct {
12961371 try waitInner(cond, io, mutex, false);
12971372 }
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`.
12991377 pub fn waitUncancelable(cond: *Condition, io: Io, mutex: *Mutex) void {
13001378 waitInner(cond, io, mutex, true) catch |err| switch (err) {
13011379 error.Canceled => unreachable,
......@@ -1424,7 +1502,9 @@ pub const Event = enum(u32) {
14241502 }
14251503 }
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`.
14281508 pub fn waitUncancelable(event: *Event, io: Io) void {
14291509 if (@cmpxchgStrong(Event, event, .unset, .waiting, .acquire, .acquire)) |prev| switch (prev) {
14301510 .unset => unreachable,
......@@ -1531,7 +1611,9 @@ pub const TypeErasedQueue = struct {
15311611 return q.putLocked(io, elements, min, false);
15321612 }
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`.
15351617 pub fn putUncancelable(q: *TypeErasedQueue, io: Io, elements: []const u8, min: usize) usize {
15361618 assert(elements.len >= min);
15371619 if (elements.len == 0) return 0;
......@@ -1602,7 +1684,10 @@ pub const TypeErasedQueue = struct {
16021684 return q.getLocked(io, buffer, min, false);
16031685 }
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 {
16061691 assert(buffer.len >= min);
16071692 if (buffer.len == 0) return 0;
16081693 q.mutex.lockUncancelable(io);
......@@ -1722,7 +1807,9 @@ pub fn Queue(Elem: type) type {
17221807 assert(try q.put(io, elements, elements.len) == elements.len);
17231808 }
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`.
17261813 pub fn putUncancelable(q: *@This(), io: Io, elements: []const Elem, min: usize) usize {
17271814 return @divExact(q.type_erased.putUncancelable(io, @ptrCast(elements), min * @sizeOf(Elem)), @sizeOf(Elem));
17281815 }
......@@ -1731,6 +1818,9 @@ pub fn Queue(Elem: type) type {
17311818 assert(try q.put(io, &.{item}, 1) == 1);
17321819 }
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`.
17341824 pub fn putOneUncancelable(q: *@This(), io: Io, item: Elem) void {
17351825 assert(q.putUncancelable(io, &.{item}, 1) == 1);
17361826 }
......@@ -1746,8 +1836,11 @@ pub fn Queue(Elem: type) type {
17461836 return @divExact(try q.type_erased.get(io, @ptrCast(buffer), min * @sizeOf(Elem)), @sizeOf(Elem));
17471837 }
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`.
17491842 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));
17511844 }
17521845
17531846 pub fn getOne(q: *@This(), io: Io) Cancelable!Elem {
......@@ -1756,6 +1849,9 @@ pub fn Queue(Elem: type) type {
17561849 return buf[0];
17571850 }
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`.
17591855 pub fn getOneUncancelable(q: *@This(), io: Io) Elem {
17601856 var buf: [1]Elem = undefined;
17611857 assert(q.getUncancelable(io, &buf, 1) == 1);
......@@ -1846,10 +1942,6 @@ pub fn concurrent(
18461942 return future;
18471943}
18481944
1849pub fn cancelRequested(io: Io) bool {
1850 return io.vtable.cancelRequested(io.userdata);
1851}
1852
18531945pub const SleepError = error{UnsupportedClock} || UnexpectedError || Cancelable;
18541946
18551947pub fn sleep(io: Io, duration: Duration, clock: Clock) SleepError!void {
lib/std/Io/Threaded.zig+73-2
......@@ -86,7 +86,9 @@ const Thread = struct {
8686 /// The value that needs to be passed to pthread_kill or tgkill in order to
8787 /// send a signal.
8888 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
9193 const SignaleeId = if (std.Thread.use_pthreads) std.c.pthread_t else std.Thread.Id;
9294
......@@ -98,6 +100,12 @@ const Thread = struct {
98100
99101 fn checkCancel(thread: *Thread) error{Canceled}!void {
100102 const closure = thread.current_closure orelse return;
103
104 switch (thread.cancel_protection) {
105 .unblocked => {},
106 .blocked => return,
107 }
108
101109 switch (@cmpxchgStrong(
102110 CancelStatus,
103111 &closure.cancel_status,
......@@ -115,6 +123,11 @@ const Thread = struct {
115123 fn beginSyscall(thread: *Thread) error{Canceled}!void {
116124 const closure = thread.current_closure orelse return;
117125
126 switch (thread.cancel_protection) {
127 .unblocked => {},
128 .blocked => return,
129 }
130
118131 switch (@cmpxchgStrong(
119132 CancelStatus,
120133 &closure.cancel_status,
......@@ -135,6 +148,12 @@ const Thread = struct {
135148
136149 fn endSyscall(thread: *Thread) void {
137150 const closure = thread.current_closure orelse return;
151
152 switch (thread.cancel_protection) {
153 .unblocked => {},
154 .blocked => return,
155 }
156
138157 _ = @cmpxchgStrong(
139158 CancelStatus,
140159 &closure.cancel_status,
......@@ -512,6 +531,8 @@ pub fn init(
512531 .have_signal_handler = false,
513532 .main_thread = .{
514533 .signal_id = Thread.currentSignalId(),
534 .current_closure = null,
535 .cancel_protection = undefined,
515536 },
516537 };
517538
......@@ -546,7 +567,11 @@ pub const init_single_threaded: Threaded = .{
546567 .old_sig_io = undefined,
547568 .old_sig_pipe = undefined,
548569 .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 },
550575};
551576
552577pub fn setAsyncLimit(t: *Threaded, new_limit: Io.Limit) void {
......@@ -581,6 +606,8 @@ fn join(t: *Threaded) void {
581606fn worker(t: *Threaded) void {
582607 var thread: Thread = .{
583608 .signal_id = Thread.currentSignalId(),
609 .current_closure = null,
610 .cancel_protection = undefined,
584611 };
585612 Thread.current = &thread;
586613
......@@ -617,6 +644,10 @@ pub fn io(t: *Threaded) Io {
617644 .groupWait = groupWait,
618645 .groupCancel = groupCancel,
619646
647 .recancel = recancel,
648 .swapCancelProtection = swapCancelProtection,
649 .checkCancel = checkCancel,
650
620651 .futexWait = futexWait,
621652 .futexWaitUncancelable = futexWaitUncancelable,
622653 .futexWake = futexWake,
......@@ -709,6 +740,10 @@ pub fn ioBasic(t: *Threaded) Io {
709740 .groupWait = groupWait,
710741 .groupCancel = groupCancel,
711742
743 .recancel = recancel,
744 .swapCancelProtection = swapCancelProtection,
745 .checkCancel = checkCancel,
746
712747 .futexWait = futexWait,
713748 .futexWaitUncancelable = futexWaitUncancelable,
714749 .futexWake = futexWake,
......@@ -794,9 +829,14 @@ const AsyncClosure = struct {
794829 fn start(closure: *Closure, t: *Threaded) void {
795830 const ac: *AsyncClosure = @alignCast(@fieldParentPtr("closure", closure));
796831 const current_thread = Thread.getCurrent(t);
832
797833 current_thread.current_closure = closure;
834 current_thread.cancel_protection = .unblocked;
835
798836 ac.func(ac.contextPointer(), ac.resultPointer());
837
799838 current_thread.current_closure = null;
839 current_thread.cancel_protection = undefined;
800840
801841 if (@atomicRmw(?*Io.Event, &ac.select_condition, .Xchg, done_event, .release)) |select_event| {
802842 assert(select_event != done_event);
......@@ -978,9 +1018,14 @@ const GroupClosure = struct {
9781018 const group = gc.group;
9791019 const group_state: *std.atomic.Value(usize) = @ptrCast(&group.state);
9801020 const event: *Io.Event = @ptrCast(&group.context);
1021
9811022 current_thread.current_closure = closure;
1023 current_thread.cancel_protection = .unblocked;
1024
9821025 gc.func(group, gc.contextPointer());
1026
9831027 current_thread.current_closure = null;
1028 current_thread.cancel_protection = undefined;
9841029
9851030 const prev_state = group_state.fetchSub(sync_one_pending, .acq_rel);
9861031 assert((prev_state / sync_one_pending) > 0);
......@@ -1201,6 +1246,32 @@ fn groupCancel(userdata: ?*anyopaque, group: *Io.Group, token: *anyopaque) void
12011246 }
12021247}
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
12041275fn await(
12051276 userdata: ?*anyopaque,
12061277 any_future: *Io.AnyFuture,
lib/std/Io/test.zig+75
......@@ -291,3 +291,78 @@ test "Event" {
291291 try std.testing.expectError(error.Canceled, future.cancel(io));
292292 }
293293}
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}