feature. See also
. The project being documented here (as the example) is the Zig library itself.
Threaded.Group
const Group = struct
File
Code
const Group = struct {
ptr: *Io.Group,
fn status(g: Group) *std.atomic.Value(Status) {
return @ptrCast(&g.ptr.token);
}
fn awaiter(g: Group) **std.atomic.Value(u32) {
return @ptrCast(&g.ptr.state);
}
const Status = packed struct(usize) {
num_running: @Int(.unsigned, @bitSizeOf(usize) - 2),
have_awaiter: bool,
canceled: bool,
};
const Task = struct {
runnable: Runnable,
group: *Io.Group,
func: *const fn (context: *const anyopaque) void,
context_alignment: Alignment,
alloc_len: usize,
fn create(
gpa: Allocator,
group: Group,
context: []const u8,
context_alignment: Alignment,
func: *const fn (context: *const anyopaque) void,
) Allocator.Error!*Task {
const max_context_misalignment = context_alignment.toByteUnits() -| @alignOf(Task);
const worst_case_context_offset = context_alignment.forward(@sizeOf(Task) + max_context_misalignment);
const alloc_len = worst_case_context_offset + context.len;
const task: *Task = @ptrCast(@alignCast(try gpa.alignedAlloc(u8, .of(Task), alloc_len)));
errdefer comptime unreachable;
task.* = .{
.runnable = .{
.node = undefined,
.startFn = &start,
},
.group = group.ptr,
.func = func,
.context_alignment = context_alignment,
.alloc_len = alloc_len,
};
@memcpy(task.contextPointer()[0..context.len], context);
return task;
}
fn destroy(task: *Task, gpa: Allocator) void {
const base: [*]align(@alignOf(Task)) u8 = @ptrCast(task);
gpa.free(base[0..task.alloc_len]);
}
fn contextPointer(task: *Task) [*]u8 {
const base: [*]u8 = @ptrCast(task);
const offset = task.context_alignment.forward(@intFromPtr(base) + @sizeOf(Task)) - @intFromPtr(base);
return base + offset;
}
fn start(r: *Runnable, thread: *Thread, t: *Threaded) void {
const task: *Task = @fieldParentPtr("runnable", r);
const group: Group = .{ .ptr = task.group };
// enforce the ordering between this and the `group.status().load` below. Paired with
// the `.release` rmw on `Thread.status` in `cancelThreads`, this creates a StoreLoad
// barrier which guarantees that when a group is canceled, either we see the cancelation
// in the group status, or the canceler sees our thread status so can directly notify us
// of the cancelation.
_ = thread.status.swap(.{
.cancelation = .none,
.awaitable = .fromGroup(group.ptr),
}, .acquire);
if (group.status().load(.monotonic).canceled) {
thread.status.store(.{
.cancelation = .canceling,
.awaitable = .fromGroup(group.ptr),
}, .monotonic);
}
task.func(task.contextPointer());
thread.status.store(.{ .cancelation = .none, .awaitable = .null }, .monotonic);
const old_status = group.status().fetchSub(.{
.num_running = 1,
.have_awaiter = false,
.canceled = false,
}, .acq_rel);
assert(old_status.num_running > 0);
if (old_status.have_awaiter and old_status.num_running == 1) {
const to_signal = group.awaiter().*;
// drop to 0 after this point would indicate that another task started up, meaning
// `async`/`cancel` was racing with awaited group completion.
group.awaiter().* = undefined;
_ = to_signal.fetchAdd(1, .release);
Thread.futexWake(&to_signal.raw, 1);
}
task.destroy(t.allocator);
}
};
fn cancelThreads(g: Group, t: *Threaded) bool {
var any_blocked = false;
var it = t.worker_threads.load(.acquire);
while (it) |thread| : (it = thread.next) {
_ = thread.status.fetchOr(.{ .cancelation = @fromBackingInt(@intCast(0)), .awaitable = .null }, .release);
if (thread.cancelAwaitable(.fromGroup(g.ptr))) any_blocked = true;
}
return any_blocked;
}
fn signalAllCanceledSyscalls(g: Group, t: *Threaded) bool {
var any_signaled = false;
var it = t.worker_threads.load(.acquire);
while (it) |thread| : (it = thread.next) {
if (thread.signalCanceledSyscall(t, .fromGroup(g.ptr))) any_signaled = true;
}
return any_signaled;
}
fn waitForCancelWithSignaling(
g: Group,
t: *Threaded,
num_completed: *std.atomic.Value(u32),
skip_signals: bool,
) void {
var need_signal: bool = !skip_signals and g.cancelThreads(t);
var timeout_ns: u64 = 1 << 10;
while (true) {
need_signal = need_signal and g.signalAllCanceledSyscalls(t);
Thread.futexWaitUncancelable(&num_completed.raw, 0, if (need_signal) timeout_ns else null);
switch (num_completed.load(.acquire)) {
0 => {},
1 => break,
else => unreachable,
}
timeout_ns <<|= 1;
}
}
}