authorgravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2025-12-21 14:51:06+00:00
committergravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2025-12-22 11:54:57+00:00
log4772f1a9f418d91e3e157b4f45a8edb7ed7975f0
tree4315cf812b9148738c3e67501b4ab2d14e896d7e
parent49f17237b0332680cebf3e4d83dedc3db46356e9
signaturelock-open Commit is signed but in an unrecognized format.

std.Io: make it safe to call `Group.async` within a group task


2 files changed, 35 insertions(+), 12 deletions(-)

lib/std/Io.zig+19-6
......@@ -1016,9 +1016,14 @@ pub fn Future(Result: type) type {
10161016pub const Group = struct {
10171017 state: usize,
10181018 context: ?*anyopaque,
1019 token: ?*anyopaque,
1019 /// This value indicates whether or not a group has pending tasks. `null`
1020 /// means there are no pending tasks, and no resources associated with the
1021 /// group, so `await` and `cancel` return immediately without calling the
1022 /// implementation. This means that `token` must be accessed atomically to
1023 /// avoid racing with the check in `await` and `cancel`.
1024 token: std.atomic.Value(?*anyopaque),
10201025
1021 pub const init: Group = .{ .state = 0, .context = null, .token = null };
1026 pub const init: Group = .{ .state = 0, .context = null, .token = .init(null) };
10221027
10231028 /// Calls `function` with `args` asynchronously. The resource spawned is
10241029 /// owned by the group.
......@@ -1081,10 +1086,14 @@ pub const Group = struct {
10811086 /// cancellation requests propagate to all members of the group.
10821087 ///
10831088 /// Idempotent. Not threadsafe.
1089 ///
1090 /// It is safe to call this function concurrently with `Group.async` or
1091 /// `Group.concurrent`, provided that the group does not complete until
1092 /// the call to `Group.async` or `Group.concurrent` returns.
10841093 pub fn wait(g: *Group, io: Io) void {
1085 const token = g.token orelse return;
1086 g.token = null;
1094 const token = g.token.load(.acquire) orelse return;
10871095 io.vtable.groupWait(io.userdata, g, token);
1096 assert(g.token.raw == null);
10881097 }
10891098
10901099 /// Equivalent to `wait` but immediately requests cancellation on all
......@@ -1093,10 +1102,14 @@ pub const Group = struct {
10931102 /// For a description of cancelation and cancelation points, see `Future.cancel`.
10941103 ///
10951104 /// Idempotent. Not threadsafe.
1105 ///
1106 /// It is safe to call this function concurrently with `Group.async` or
1107 /// `Group.concurrent`, provided that the group does not complete until
1108 /// the call to `Group.async` or `Group.concurrent` returns.
10961109 pub fn cancel(g: *Group, io: Io) void {
1097 const token = g.token orelse return;
1098 g.token = null;
1110 const token = g.token.load(.acquire) orelse return;
10991111 io.vtable.groupCancel(io.userdata, g, token);
1112 assert(g.token.raw == null);
11001113 }
11011114};
11021115
lib/std/Io/Threaded.zig+16-6
......@@ -1117,8 +1117,8 @@ fn groupAsync(
11171117 }
11181118
11191119 // Append to the group linked list inside the mutex to make `Io.Group.async` thread-safe.
1120 gc.node = .{ .next = @ptrCast(@alignCast(group.token)) };
1121 group.token = &gc.node;
1120 gc.node = .{ .next = @ptrCast(@alignCast(group.token.load(.monotonic))) };
1121 group.token.store(&gc.node, .monotonic);
11221122
11231123 t.run_queue.prepend(&gc.closure.node);
11241124
......@@ -1169,8 +1169,8 @@ fn groupConcurrent(
11691169 }
11701170
11711171 // Append to the group linked list inside the mutex to make `Io.Group.concurrent` thread-safe.
1172 gc.node = .{ .next = @ptrCast(@alignCast(group.token)) };
1173 group.token = &gc.node;
1172 gc.node = .{ .next = @ptrCast(@alignCast(group.token.load(.monotonic))) };
1173 group.token.store(&gc.node, .monotonic);
11741174
11751175 t.run_queue.prepend(&gc.closure.node);
11761176
......@@ -1187,7 +1187,7 @@ fn groupWait(userdata: ?*anyopaque, group: *Io.Group, token: *anyopaque) void {
11871187 const t: *Threaded = @ptrCast(@alignCast(userdata));
11881188 const gpa = t.allocator;
11891189
1190 if (builtin.single_threaded) return;
1190 if (builtin.single_threaded) unreachable; // we never set `group.token` to non-`null`
11911191
11921192 const group_state: *std.atomic.Value(usize) = @ptrCast(&group.state);
11931193 const event: *Io.Event = @ptrCast(&group.context);
......@@ -1212,13 +1212,18 @@ fn groupWait(userdata: ?*anyopaque, group: *Io.Group, token: *anyopaque) void {
12121212 gc.deinit(gpa);
12131213 node = node_next orelse break;
12141214 }
1215
1216 // Since the group has now finished, it's illegal to add more tasks to it until we return. It's
1217 // also illegal for us to race with another `await` or `cancel`. Therefore, we must be the only
1218 // thread who can access `group` right now.
1219 group.token.raw = null;
12151220}
12161221
12171222fn groupCancel(userdata: ?*anyopaque, group: *Io.Group, token: *anyopaque) void {
12181223 const t: *Threaded = @ptrCast(@alignCast(userdata));
12191224 const gpa = t.allocator;
12201225
1221 if (builtin.single_threaded) return;
1226 if (builtin.single_threaded) unreachable; // we never set `group.token` to non-`null`
12221227
12231228 {
12241229 var node: *std.SinglyLinkedList.Node = @ptrCast(@alignCast(token));
......@@ -1244,6 +1249,11 @@ fn groupCancel(userdata: ?*anyopaque, group: *Io.Group, token: *anyopaque) void
12441249 node = node_next orelse break;
12451250 }
12461251 }
1252
1253 // Since the group has now finished, it's illegal to add more tasks to it until we return. It's
1254 // also illegal for us to race with another `await` or `cancel`. Therefore, we must be the only
1255 // thread who can access `group` right now.
1256 group.token.raw = null;
12471257}
12481258
12491259fn recancel(userdata: ?*anyopaque) void {