authorgravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2025-12-30 21:46:57+00:00
committergravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2026-01-03 15:45:11+00:00
logb8a09bcbd955c2d97216ffef23246054aa3fe756
tree5f53c58e6411ad5d526994c67056105f667e841b
parent2c395e326f2db4e2389af729b88bd93e76e4d27c
signaturelock-open Commit is signed but in an unrecognized format.

std.Io.Group: tweak documentation and vtable API


2 files changed, 47 insertions(+), 55 deletions(-)

lib/std/Io.zig+38-46
......@@ -631,7 +631,7 @@ pub const VTable = struct {
631631 /// Copied and then passed to `start`.
632632 context: []const u8,
633633 context_alignment: std.mem.Alignment,
634 start: *const fn (*Group, context: *const anyopaque) Cancelable!void,
634 start: *const fn (context: *const anyopaque) Cancelable!void,
635635 ) void,
636636 /// Thread-safe.
637637 groupConcurrent: *const fn (
......@@ -642,7 +642,7 @@ pub const VTable = struct {
642642 /// Copied and then passed to `start`.
643643 context: []const u8,
644644 context_alignment: std.mem.Alignment,
645 start: *const fn (*Group, context: *const anyopaque) Cancelable!void,
645 start: *const fn (context: *const anyopaque) Cancelable!void,
646646 ) ConcurrentError!void,
647647 groupAwait: *const fn (?*anyopaque, *Group, token: *anyopaque) Cancelable!void,
648648 groupCancel: *const fn (?*anyopaque, *Group, token: *anyopaque) void,
......@@ -1050,40 +1050,40 @@ pub fn Future(Result: type) type {
10501050 };
10511051}
10521052
1053/// An unordered set of tasks which can only be awaited or canceled as a whole.
1054/// Tasks are spawned in the group with `Group.async` and `Group.concurrent`.
1055///
1056/// The resources associated with each task are *guaranteed* to be released when
1057/// the individual task returns, as opposed to when the whole group completes or
1058/// is awaited. For this reason, it is not a resource leak to have a long-lived
1059/// group which concurrent tasks are repeatedly added to. However, asynchronous
1060/// tasks are not guaranteed to run until `Group.await` or `Group.cancel` is
1061/// called, so adding async tasks to a group without ever awaiting it may leak
1062/// resources.
10531063pub const Group = struct {
1054 state: usize,
1055 context: ?*anyopaque,
10561064 /// This value indicates whether or not a group has pending tasks. `null`
10571065 /// means there are no pending tasks, and no resources associated with the
10581066 /// group, so `await` and `cancel` return immediately without calling the
10591067 /// implementation. This means that `token` must be accessed atomically to
10601068 /// avoid racing with the check in `await` and `cancel`.
10611069 token: std.atomic.Value(?*anyopaque),
1070 /// This value is available for the implementation to use as it wishes.
1071 state: usize,
10621072
1063 pub const init: Group = .{ .state = 0, .context = null, .token = .init(null) };
1073 pub const init: Group = .{ .token = .init(null), .state = 0 };
10641074
1065 /// Calls `function` with `args` asynchronously. The resource spawned is
1066 /// owned by the group.
1067 ///
1068 /// `function` *may* be called immediately, before `async` returns.
1075 /// Equivalent to `Io.async`, except the task is spawned in this `Group`
1076 /// instead of becoming associated with a `Future`.
10691077 ///
1070 /// When this function returns, it is guaranteed that `function` has
1071 /// already been called and completed, or it has successfully been assigned
1072 /// a unit of concurrency.
1078 /// The return type of `function` must be coercible to `Cancelable!void`.
10731079 ///
1074 /// After this is called, `await` or `cancel` must be called before the
1075 /// group is deinitialized.
1076 ///
1077 /// Threadsafe.
1078 ///
1079 /// See also:
1080 /// * `concurrent`
1081 /// * `Io.async`
1080 /// Once this function is called, there are resources associated with the
1081 /// group. To release those resources, `Group.await` or `Group.cancel` must
1082 /// eventually be called.
10821083 pub fn async(g: *Group, io: Io, function: anytype, args: std.meta.ArgsTuple(@TypeOf(function))) void {
10831084 const Args = @TypeOf(args);
10841085 const TypeErased = struct {
1085 fn start(group: *Group, context: *const anyopaque) Cancelable!void {
1086 _ = group;
1086 fn start(context: *const anyopaque) Cancelable!void {
10871087 const args_casted: *const Args = @ptrCast(@alignCast(context));
10881088 return @call(.auto, function, args_casted.*);
10891089 }
......@@ -1091,27 +1091,18 @@ pub const Group = struct {
10911091 io.vtable.groupAsync(io.userdata, g, @ptrCast(&args), .of(Args), TypeErased.start);
10921092 }
10931093
1094 /// Calls `function` with `args`, such that the function is not guaranteed
1095 /// to have returned until `await` is called, allowing the caller to
1096 /// progress while waiting for any `Io` operations.
1097 ///
1098 /// The resource spawned is owned by the group; after this is called,
1099 /// `await` or `cancel` must be called before the group is deinitialized.
1100 ///
1101 /// This has stronger guarantee than `async`, placing restrictions on what kind
1102 /// of `Io` implementations are supported. By calling `async` instead, one
1103 /// allows, for example, stackful single-threaded blocking I/O.
1094 /// Equivalent to `Io.concurrent`, except the task is spawned in this
1095 /// `Group` instead of becoming associated with a `Future`.
11041096 ///
1105 /// Threadsafe.
1097 /// The return type of `function` must be coercible to `Cancelable!void`.
11061098 ///
1107 /// See also:
1108 /// * `async`
1109 /// * `Io.concurrent`
1099 /// Once this function is called, there are resources associated with the
1100 /// group. To release those resources, `Group.await` or `Group.cancel` must
1101 /// eventually be called.
11101102 pub fn concurrent(g: *Group, io: Io, function: anytype, args: std.meta.ArgsTuple(@TypeOf(function))) ConcurrentError!void {
11111103 const Args = @TypeOf(args);
11121104 const TypeErased = struct {
1113 fn start(group: *Group, context: *const anyopaque) Cancelable!void {
1114 _ = group;
1105 fn start(context: *const anyopaque) Cancelable!void {
11151106 const args_casted: *const Args = @ptrCast(@alignCast(context));
11161107 return @call(.auto, function, args_casted.*);
11171108 }
......@@ -1263,19 +1254,20 @@ pub fn Select(comptime U: type) type {
12631254 function: anytype,
12641255 args: std.meta.ArgsTuple(@TypeOf(function)),
12651256 ) void {
1266 const Args = @TypeOf(args);
1267 const TypeErased = struct {
1268 fn start(group: *Group, context: *const anyopaque) Cancelable!void {
1269 const args_casted: *const Args = @ptrCast(@alignCast(context));
1270 const unerased_select: *S = @fieldParentPtr("group", group);
1271 const elem = @unionInit(U, @tagName(field), @call(.auto, function, args_casted.*));
1272 unerased_select.queue.putOneUncancelable(unerased_select.io, elem) catch |err| switch (err) {
1257 const Context = struct {
1258 select: *S,
1259 args: @TypeOf(args),
1260 fn start(type_erased_context: *const anyopaque) Cancelable!void {
1261 const context: *const @This() = @ptrCast(@alignCast(type_erased_context));
1262 const elem = @unionInit(U, @tagName(field), @call(.auto, function, context.args));
1263 context.select.queue.putOneUncancelable(context.select.io, elem) catch |err| switch (err) {
12731264 error.Closed => unreachable,
12741265 };
12751266 }
12761267 };
1268 const context: Context = .{ .select = s, .args = args };
12771269 _ = @atomicRmw(usize, &s.outstanding, .Add, 1, .monotonic);
1278 s.io.vtable.groupAsync(s.io.userdata, &s.group, @ptrCast(&args), .of(Args), TypeErased.start);
1270 s.io.vtable.groupAsync(s.io.userdata, &s.group, @ptrCast(&context), .of(Context), Context.start);
12791271 }
12801272
12811273 /// Blocks until another task of the select finishes.
lib/std/Io/Threaded.zig+9-9
......@@ -182,7 +182,7 @@ const Group = struct {
182182 const Task = struct {
183183 runnable: Runnable,
184184 group: *Io.Group,
185 func: *const fn (*Io.Group, context: *const anyopaque) void,
185 func: *const fn (context: *const anyopaque) void,
186186 context_alignment: Alignment,
187187 alloc_len: usize,
188188
......@@ -192,7 +192,7 @@ const Group = struct {
192192 group: Group,
193193 context: []const u8,
194194 context_alignment: Alignment,
195 func: *const fn (*Io.Group, context: *const anyopaque) void,
195 func: *const fn (context: *const anyopaque) void,
196196 ) Allocator.Error!*Task {
197197 const max_context_misalignment = context_alignment.toByteUnits() -| @alignOf(Task);
198198 const worst_case_context_offset = context_alignment.forward(@sizeOf(Task) + max_context_misalignment);
......@@ -247,7 +247,7 @@ const Group = struct {
247247 }, .monotonic);
248248 }
249249
250 assertGroupResult(task.func(group.ptr, task.contextPointer()));
250 assertGroupResult(task.func(task.contextPointer()));
251251
252252 thread.status.store(.{ .cancelation = .none, .awaitable = .null }, .monotonic);
253253 const old_status = group.status().fetchSub(.{
......@@ -1707,16 +1707,16 @@ fn groupAsync(
17071707 type_erased: *Io.Group,
17081708 context: []const u8,
17091709 context_alignment: Alignment,
1710 start: *const fn (*Io.Group, context: *const anyopaque) Io.Cancelable!void,
1710 start: *const fn (context: *const anyopaque) Io.Cancelable!void,
17111711) void {
17121712 const t: *Threaded = @ptrCast(@alignCast(userdata));
17131713 const g: Group = .{ .ptr = type_erased };
17141714
1715 if (builtin.single_threaded) return start(g.ptr, context.ptr) catch unreachable;
1715 if (builtin.single_threaded) return start(context.ptr) catch unreachable;
17161716
17171717 const gpa = t.allocator;
17181718 const task = Group.Task.create(gpa, g, context, context_alignment, start) catch |err| switch (err) {
1719 error.OutOfMemory => return t.assertGroupResult(start(g.ptr, context.ptr)),
1719 error.OutOfMemory => return t.assertGroupResult(start(context.ptr)),
17201720 };
17211721
17221722 t.mutex.lock();
......@@ -1726,7 +1726,7 @@ fn groupAsync(
17261726 if (busy_count >= @intFromEnum(t.async_limit)) {
17271727 t.mutex.unlock();
17281728 task.destroy(gpa);
1729 return t.assertGroupResult(start(g.ptr, context.ptr));
1729 return t.assertGroupResult(start(context.ptr));
17301730 }
17311731
17321732 t.busy_count = busy_count + 1;
......@@ -1739,7 +1739,7 @@ fn groupAsync(
17391739 t.busy_count = busy_count;
17401740 t.mutex.unlock();
17411741 task.destroy(gpa);
1742 return t.assertGroupResult(start(g.ptr, context.ptr));
1742 return t.assertGroupResult(start(context.ptr));
17431743 };
17441744 thread.detach();
17451745 }
......@@ -1782,7 +1782,7 @@ fn groupConcurrent(
17821782 type_erased: *Io.Group,
17831783 context: []const u8,
17841784 context_alignment: Alignment,
1785 start: *const fn (*Io.Group, context: *const anyopaque) Io.Cancelable!void,
1785 start: *const fn (context: *const anyopaque) Io.Cancelable!void,
17861786) Io.ConcurrentError!void {
17871787 if (builtin.single_threaded) return error.ConcurrencyUnavailable;
17881788