authorgravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2025-12-23 20:54:50+00:00
committergravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2026-01-03 15:45:07+00:00
log0da5d5f15063455eaab9a350352914a062cf18e2
tree9191420c06679315cbd9de47c71d685775339a16
parent04226193ccb69f50936e47804be56bd1bdc316d9
signaturelock-open Commit is signed but in an unrecognized format.

std.Io.Threaded: rework cancelation and groups

The goal of this internal refactor is to fix some bugs in cancelation and allow group tasks to clean up their own resources eagerly. The latter will become a guarantee of the `std.Io` interface, which is important so that groups can be used to "detach" tasks. This commit changes the API which POSIX system calls use internally (the functions formerly called `beginSyscall` etc), but does not update the usage sites yet.

1 files changed, 918 insertions(+), 510 deletions(-)

lib/std/Io/Threaded.zig+918-510
......@@ -37,7 +37,7 @@ cpu_count_error: ?std.Thread.CpuCountError,
3737/// available count, subtract this from either `async_limit` or
3838/// `concurrent_limit`.
3939busy_count: usize = 0,
40main_thread: Thread,
40worker_threads: std.atomic.Value(?*Thread),
4141pid: Pid = .unknown,
4242robust_cancel: RobustCancel,
4343
......@@ -153,107 +153,465 @@ pub const UseFchmodat2 = if (have_fchmodat2 and !have_fchmodat_flags) enum {
153153 pub const default: UseFchmodat2 = .disabled;
154154};
155155
156const Thread = struct {
157 /// The value that needs to be passed to pthread_kill or tgkill in order to
158 /// send a signal.
159 signal_id: SignaleeId,
160 current_closure: ?*Closure,
161 /// Only populated if `current_closure != null`. Indicates the current cancel protection mode.
162 cancel_protection: Io.CancelProtection,
163
164 const SignaleeId = if (std.Thread.use_pthreads) std.c.pthread_t else std.Thread.Id;
156const Runnable = struct {
157 node: std.SinglyLinkedList.Node,
158 startFn: *const fn (*Runnable, *Thread, *Threaded) void,
159};
165160
166 threadlocal var current: ?*Thread = null;
161const Group = struct {
162 ptr: *Io.Group,
167163
168 fn getCurrent(t: *Threaded) *Thread {
169 return current orelse return &t.main_thread;
164 /// Returns a correctly-typed pointer to the `Io.Group.token` field.
165 ///
166 /// The status indicates how many pending tasks are in the group, whether the group has been
167 /// canceled, and whether the group has been awaited.
168 ///
169 /// Note that the zero value of `Status` intentionally represents the initial group state (empty
170 /// with no awaiters). This is a requirement of `Io.Group`.
171 fn status(g: Group) *std.atomic.Value(Status) {
172 return @ptrCast(&g.ptr.token);
173 }
174 /// Returns a correctly-typed pointer to the `Io.Group.state` field. The double-pointer here is
175 /// intentional, because the `state` field itself stores a pointer, and this function returns a
176 /// pointer to that field.
177 ///
178 /// On completion of the whole group, if `status` indicates that there is an awaiter, the last
179 /// task must increment this `u32` and do a futex wake on it to signal that awaiter.
180 fn awaiter(g: Group) **std.atomic.Value(u32) {
181 return @ptrCast(&g.ptr.state);
170182 }
171183
172 fn checkCancel(thread: *Thread) error{Canceled}!void {
173 const closure = thread.current_closure orelse return;
184 const Status = packed struct(usize) {
185 num_running: @Int(.unsigned, @bitSizeOf(usize) - 2),
186 have_awaiter: bool,
187 canceled: bool,
188 };
174189
175 switch (thread.cancel_protection) {
176 .unblocked => {},
177 .blocked => return,
190 const Task = struct {
191 runnable: Runnable,
192 group: *Io.Group,
193 func: *const fn (*Io.Group, context: *const anyopaque) void,
194 context_alignment: Alignment,
195 alloc_len: usize,
196
197 /// `Task.runnable.node` is `undefined` in the created `Task`.
198 fn create(
199 gpa: Allocator,
200 group: Group,
201 context: []const u8,
202 context_alignment: Alignment,
203 func: *const fn (*Io.Group, context: *const anyopaque) void,
204 ) Allocator.Error!*Task {
205 const max_context_misalignment = context_alignment.toByteUnits() -| @alignOf(Task);
206 const worst_case_context_offset = context_alignment.forward(@sizeOf(Task) + max_context_misalignment);
207 const alloc_len = worst_case_context_offset + context.len;
208
209 const task: *Task = @ptrCast(@alignCast(try gpa.alignedAlloc(u8, .of(Task), alloc_len)));
210 errdefer comptime unreachable;
211
212 task.* = .{
213 .runnable = .{
214 .node = undefined,
215 .startFn = &start,
216 },
217 .group = group.ptr,
218 .func = func,
219 .context_alignment = context_alignment,
220 .alloc_len = alloc_len,
221 };
222 @memcpy(task.contextPointer()[0..context.len], context);
223 return task;
224 }
225
226 fn destroy(task: *Task, gpa: Allocator) void {
227 const base: [*]align(@alignOf(Task)) u8 = @ptrCast(task);
228 gpa.free(base[0..task.alloc_len]);
229 }
230
231 fn contextPointer(task: *Task) [*]u8 {
232 const base: [*]u8 = @ptrCast(task);
233 const offset = task.context_alignment.forward(@intFromPtr(base) + @sizeOf(Task)) - @intFromPtr(base);
234 return base + offset;
235 }
236
237 fn start(r: *Runnable, thread: *Thread, t: *Threaded) void {
238 const task: *Task = @fieldParentPtr("runnable", r);
239 const group: Group = .{ .ptr = task.group };
240
241 // This would be a simple store, but it's upgraded to an RMW so we can use `.acquire` to
242 // enforce the ordering between this and the `group.status().load` below. Paired with
243 // the `.release` rmw on `Thread.status` in `cancelThreads`, this creates a StoreLoad
244 // barrier which guarantees that when a group is canceled, either we see the cancelation
245 // in the group status, or the canceler sees our thread status so can directly notify us
246 // of the cancelation.
247 _ = thread.status.swap(.{
248 .cancelation = .none,
249 .awaitable = .fromGroup(group.ptr),
250 }, .acquire);
251 if (group.status().load(.monotonic).canceled) {
252 thread.status.store(.{
253 .cancelation = .canceling,
254 .awaitable = .fromGroup(group.ptr),
255 }, .monotonic);
256 }
257
258 assertGroupResult(task.func(group.ptr, task.contextPointer()));
259
260 thread.status.store(.{ .cancelation = .none, .awaitable = .null }, .monotonic);
261 const old_status = group.status().fetchSub(.{
262 .num_running = 1,
263 .have_awaiter = false,
264 .canceled = false,
265 }, .acq_rel); // acquire `group.awaiter()`, release task results
266 assert(old_status.num_running > 0);
267 if (old_status.have_awaiter and old_status.num_running == 1) {
268 const to_signal = group.awaiter().*;
269 // `awaiter` should only be modified by us. For another thread to see `num_running`
270 // drop to 0 after this point would indicate that another task started up, meaning
271 // `async`/`cancel` was racing with awaited group completion.
272 group.awaiter().* = undefined;
273 _ = to_signal.fetchAdd(1, .release); // release results
274 Thread.futexWake(&to_signal.raw, 1);
275 }
276
277 // Task completed. Self-destruct sequence initiated.
278 task.destroy(t.allocator);
178279 }
280 };
179281
180 switch (@cmpxchgStrong(
181 CancelStatus,
182 &closure.cancel_status,
183 .requested,
184 .acknowledged,
185 .acq_rel,
186 .acquire,
187 ) orelse return error.Canceled) {
188 .requested => unreachable,
189 .acknowledged => unreachable,
190 .none, _ => {},
282 /// Assumes the caller has already atomically updated the group status to indicate cancelation,
283 /// and notifies any already-running threads of this cancelation.
284 fn cancelThreads(g: Group, t: *Threaded) bool {
285 var any_blocked = false;
286 var it = t.worker_threads.load(.acquire); // acquire `Thread` values
287 while (it) |thread| : (it = thread.next) {
288 // This non-mutating RMW exists for ordering reasons: see comment in `Group.Task.start` for reasons.
289 _ = thread.status.fetchOr(.{ .cancelation = @enumFromInt(0), .awaitable = .null }, .release);
290 if (thread.cancelAwaitable(.fromGroup(g.ptr))) any_blocked = true;
191291 }
292 return any_blocked;
192293 }
193294
194 fn beginSyscall(thread: *Thread) error{Canceled}!void {
195 const closure = thread.current_closure orelse return;
295 /// Uses `Thread.signalCanceledSyscall` to signal any threads which are still blocked in a
296 /// syscall for this group and have not observed a cancelation request yet. Returns `true` if
297 /// more signals may be necessary, in which case the caller must call this again after a delay.
298 fn signalAllCanceledSyscalls(g: Group, t: *Threaded) bool {
299 var any_signaled = false;
300 var it = t.worker_threads.load(.acquire); // acquire `Thread` values
301 while (it) |thread| : (it = thread.next) {
302 if (thread.signalCanceledSyscall(t, .fromGroup(g.ptr))) any_signaled = true;
303 }
304 return any_signaled;
305 }
196306
197 switch (thread.cancel_protection) {
198 .unblocked => {},
199 .blocked => return,
307 /// The caller has canceled `g`. Inform any threads working on that group of the cancelation if
308 /// necessary, and wait for `g` to finish (indicated by `num_completed` being incremented from 0
309 /// to 1), while sending regular signals to threads if necessary for them to unblock from any
310 /// cancelable syscalls.
311 ///
312 /// `skip_signals` means it is already known that no threads are currently working on the group
313 /// so no notifications or signals are necessary.
314 fn waitForCancelWithSignaling(
315 g: Group,
316 t: *Threaded,
317 num_completed: *std.atomic.Value(u32),
318 skip_signals: bool,
319 ) void {
320 var need_signal: bool = !skip_signals and g.cancelThreads(t);
321 var timeout_ns: u64 = 1 << 10;
322 while (true) {
323 need_signal = need_signal and g.signalAllCanceledSyscalls(t) and t.robust_cancel == .enabled;
324 Thread.futexWaitTimed(
325 null,
326 &num_completed.raw,
327 0,
328 if (need_signal) timeout_ns else null,
329 ) catch |err| switch (err) {
330 error.Canceled => unreachable,
331 };
332 switch (num_completed.load(.acquire)) { // acquire task results
333 0 => {},
334 1 => break,
335 else => unreachable,
336 }
337 timeout_ns <<|= 1;
200338 }
339 }
340};
201341
202 switch (@cmpxchgStrong(
203 CancelStatus,
204 &closure.cancel_status,
205 .none,
206 .fromSignaleeId(thread.signal_id),
207 .acq_rel,
208 .acquire,
209 ) orelse return) {
210 .none => unreachable,
211 .requested => {
212 @atomicStore(CancelStatus, &closure.cancel_status, .acknowledged, .release);
213 return error.Canceled;
342/// Trailing data:
343/// 1. context
344/// 2. result
345const Future = struct {
346 runnable: Runnable,
347 func: *const fn (context: *const anyopaque, result: *anyopaque) void,
348 status: std.atomic.Value(Status),
349 /// On completion, increment this `u32` and do a futex wake on it.
350 awaiter: *std.atomic.Value(u32),
351 context_alignment: Alignment,
352 result_offset: usize,
353 alloc_len: usize,
354
355 const Status = packed struct(usize) {
356 /// The values of this enum are chosen so that await/cancel can just OR with 0b01 and 0b11
357 /// respectively. That *does* clobber `.done`, but that's actually fine, because if the tag
358 /// is `.done` then only the awaiter is referencing this `Future` anyway.
359 tag: enum(u2) {
360 /// The future is queued or running (depending on whether `thread` is set).
361 pending = 0b00,
362 /// Like `pending`, but the future is being awaited. `Future.awaiter` is populated.
363 pending_awaited = 0b01,
364 /// Like `pending`, but the future is being canceled. `Future.awaiter` is populated.
365 pending_canceled = 0b11,
366 /// The future has already completed. `thread` is `null`.
367 done = 0b10,
368 },
369 /// When the future begins execution, this is atomically updated from `null` to the thread running the
370 /// `Future`, so that cancelation knows which thread to cancel.
371 thread: Thread.PackedPtr,
372 };
373
374 /// `Future.runnable.node` is `undefined` in the created `Future`.
375 fn create(
376 gpa: Allocator,
377 result_len: usize,
378 result_alignment: Alignment,
379 context: []const u8,
380 context_alignment: Alignment,
381 func: *const fn (context: *const anyopaque, result: *anyopaque) void,
382 ) Allocator.Error!*Future {
383 const max_context_misalignment = context_alignment.toByteUnits() -| @alignOf(Future);
384 const worst_case_context_offset = context_alignment.forward(@sizeOf(Future) + max_context_misalignment);
385 const worst_case_result_offset = result_alignment.forward(worst_case_context_offset + context.len);
386 const alloc_len = worst_case_result_offset + result_len;
387
388 const future: *Future = @ptrCast(@alignCast(try gpa.alignedAlloc(u8, .of(Future), alloc_len)));
389 errdefer comptime unreachable;
390
391 const actual_context_addr = context_alignment.forward(@intFromPtr(future) + @sizeOf(Future));
392 const actual_result_addr = result_alignment.forward(actual_context_addr + context.len);
393 const actual_result_offset = actual_result_addr - @intFromPtr(future);
394 future.* = .{
395 .runnable = .{
396 .node = undefined,
397 .startFn = &start,
214398 },
215 .acknowledged => return,
216 _ => unreachable,
217 }
399 .func = func,
400 .status = .init(.{
401 .tag = .pending,
402 .thread = .null,
403 }),
404 .awaiter = undefined,
405 .context_alignment = context_alignment,
406 .result_offset = actual_result_offset,
407 .alloc_len = alloc_len,
408 };
409 @memcpy(future.contextPointer()[0..context.len], context);
410 return future;
218411 }
219412
220 fn endSyscall(thread: *Thread) void {
221 const closure = thread.current_closure orelse return;
413 fn destroy(future: *Future, gpa: Allocator) void {
414 const base: [*]align(@alignOf(Future)) u8 = @ptrCast(future);
415 gpa.free(base[0..future.alloc_len]);
416 }
222417
223 switch (thread.cancel_protection) {
224 .unblocked => {},
225 .blocked => return,
226 }
418 fn resultPointer(future: *Future) [*]u8 {
419 const base: [*]u8 = @ptrCast(future);
420 return base + future.result_offset;
421 }
227422
228 _ = @cmpxchgStrong(
229 CancelStatus,
230 &closure.cancel_status,
231 .fromSignaleeId(thread.signal_id),
232 .none,
233 .acq_rel,
234 .acquire,
235 ) orelse return;
423 fn contextPointer(future: *Future) [*]u8 {
424 const base: [*]u8 = @ptrCast(future);
425 const context_offset = future.context_alignment.forward(@intFromPtr(future) + @sizeOf(Future)) - @intFromPtr(future);
426 return base + context_offset;
236427 }
237428
238 fn endSyscallErrnoBug(thread: *Thread, err: posix.E) Io.UnexpectedError {
239 @branchHint(.cold);
240 thread.endSyscall();
241 return errnoBug(err);
429 fn start(r: *Runnable, thread: *Thread, t: *Threaded) void {
430 _ = t;
431 const future: *Future = @fieldParentPtr("runnable", r);
432
433 thread.status.store(.{
434 .cancelation = .none,
435 .awaitable = .fromFuture(future),
436 }, .monotonic);
437 {
438 const old_status = future.status.fetchOr(.{
439 .tag = .pending,
440 .thread = .pack(thread),
441 }, .release);
442 assert(old_status.thread == .null);
443 switch (old_status.tag) {
444 .pending, .pending_awaited => {},
445 .pending_canceled => thread.status.store(.{
446 .cancelation = .canceling,
447 .awaitable = .fromFuture(future),
448 }, .monotonic),
449 .done => unreachable,
450 }
451 }
452
453 future.func(future.contextPointer(), future.resultPointer());
454
455 thread.status.store(.{ .cancelation = .none, .awaitable = .null }, .monotonic);
456 const old_status = future.status.swap(.{
457 .tag = .done,
458 .thread = .null,
459 }, .acq_rel); // acquire `future.awaiter`, release results
460 switch (old_status.tag) {
461 .pending => {},
462 .pending_awaited, .pending_canceled => {
463 const to_signal = future.awaiter;
464 _ = to_signal.fetchAdd(1, .release); // release results
465 Thread.futexWake(&to_signal.raw, 1);
466 },
467 .done => unreachable,
468 }
469 }
470
471 /// The caller has canceled `future`. `thread` is the thread currently running that future.
472 /// Inform `thread` of the cancelation if necessary, and wait for `future` to finish (indicated
473 /// by `num_completed` being incremented from 0 to 1), while sending regular signals to `thread`
474 /// if necessary for it to unblock from a cancelable syscall.
475 fn waitForCancelWithSignaling(
476 future: *Future,
477 t: *Threaded,
478 num_completed: *std.atomic.Value(u32),
479 thread: ?*Thread,
480 ) void {
481 var need_signal: bool = thread != null and thread.?.cancelAwaitable(.fromFuture(future));
482 var timeout_ns: u64 = 1 << 10;
483 while (true) {
484 need_signal = need_signal and thread.?.signalCanceledSyscall(t, .fromFuture(future)) and t.robust_cancel == .enabled;
485 Thread.futexWaitTimed(
486 null,
487 &num_completed.raw,
488 0,
489 if (need_signal) timeout_ns else null,
490 ) catch |err| switch (err) {
491 error.Canceled => unreachable,
492 };
493 switch (num_completed.load(.acquire)) { // acquire task results
494 0 => {},
495 1 => break,
496 else => unreachable,
497 }
498 timeout_ns <<|= 1;
499 }
242500 }
501};
243502
244 fn endSyscallUnexpectedErrno(thread: *Thread, err: posix.E) Io.UnexpectedError {
245 @branchHint(.cold);
246 thread.endSyscall();
247 return posix.unexpectedErrno(err);
503/// A sequence of (ptr_bit_width - 3) bits which uniquely identifies a group or future. The bits are
504/// the MSBs of the `*Io.Group` or `*Future`. These things do not necessarily have 3 zero bits at
505/// the end (they are pointer-aligned, so on 32-bit targets only have 2), but because they both have
506/// a *size* of at least 8 bytes, no two groups/futures in memory at the same time will have the
507/// same value for all of these bits. In other words, given a group/future pointer, the next group
508/// or future must be at least 8 bytes later, so its address will have a different value for one of
509/// the top (ptr_bit_width - 3) bits.
510const AwaitableId = enum(@Int(.unsigned, @bitSizeOf(usize) - 3)) {
511 comptime {
512 assert(@sizeOf(Future) >= 8);
513 assert(@sizeOf(Io.Group) >= 8);
514 }
515 null = 0,
516 all_ones = std.math.maxInt(@Int(.unsigned, @bitSizeOf(usize) - 3)),
517 _,
518 const Split = packed struct(usize) { low: u3, high: AwaitableId };
519 fn fromGroup(g: *Io.Group) AwaitableId {
520 const split: Split = @bitCast(@intFromPtr(g));
521 return split.high;
248522 }
523 fn fromFuture(f: *Future) AwaitableId {
524 const split: Split = @bitCast(@intFromPtr(f));
525 return split.high;
526 }
527};
249528
250 /// inline to make error return traces slightly shallower.
251 inline fn endSyscallError(thread: *Thread, err: anytype) @TypeOf(err) {
252 thread.endSyscall();
253 return err;
529const Thread = struct {
530 next: ?*Thread,
531 /// The value that needs to be passed to pthread_kill or tgkill in order to
532 /// send a signal.
533 signalee_id: SignaleeId,
534
535 status: std.atomic.Value(Status),
536
537 cancel_protection: Io.CancelProtection,
538
539 const Status = packed struct(usize) {
540 /// The specific values of these enum fields are chosen to simplify the implementation of
541 /// the transformations we need to apply to this state.
542 cancelation: enum(u3) {
543 /// The thread has not yet been canceled, and is not in a cancelable operation.
544 /// To request cancelation, just set the status to `.canceling`.
545 none = 0b000,
546
547 /// The thread is parked in a cancelable futex wait or sleep.
548 /// Only applicable on Windows, NetBSD, and Illumos.
549 /// To request cancelation, set the status to `.canceling` and unpark the thread.
550 /// To unpark for another reason (futex wake), set the status to `.none` and unpark the thread.
551 parked = 0b001,
552
553 /// The thread is blocked in a cancelable system call.
554 /// To request cancelation, set the status to `.blocked_canceling` and repeatedly interrupt the system call until the status changes.
555 blocked = 0b011,
556
557 /// Windows-only: the thread is blocked on a DNS query.
558 /// To request cancelation, set the status to `.canceling` and call `DnsCancelQuery`.
559 blocked_windows_dns = 0b010,
560
561 /// The thread has an outstanding cancelation request but is not in a cancelable operation.
562 /// When it acknowledges the cancelation, it will set the status to `.canceled`.
563 canceling = 0b110,
564
565 /// The thread has received and acknowledged a cancelation request.
566 /// If `recancel` is called, the status will revert to `.canceling`, but otherwise, the status
567 /// will not change for the remainder of this task's execution.
568 canceled = 0b111,
569
570 /// The thread is blocked in a cancelable system call, and is being canceled. The thread which triggered the cancelation will send signals to this thread
571 /// until its status changes.
572 blocked_canceling = 0b101,
573 },
574
575 /// We cannot turn this value back into a pointer. Instead, it exists so that a task can be
576 /// canceled by a cmpxchg on thread status: if it is running the task we want to cancel,
577 /// then update the `cancelation` field.
578 awaitable: AwaitableId,
579 };
580
581 const SignaleeId = if (std.Thread.use_pthreads) std.c.pthread_t else std.Thread.Id;
582
583 threadlocal var current: ?*Thread = null;
584
585 /// The thread is neither in a syscall nor entering one, but we want to check for cancelation
586 /// anyway. If there is a pending cancel request, acknowledge it and return `error.Canceled`.
587 fn checkCancel() Io.Cancelable!void {
588 const thread = Thread.current orelse return;
589 switch (thread.cancel_protection) {
590 .blocked => return,
591 .unblocked => {},
592 }
593 // Here, unlike `Syscall.checkCancel`, it's not particularly likely that we're canceled, so
594 // it seems preferable to do a cheap atomic load and, in the unlikely case, a separate store
595 // to acknowledge. Besides, the state transitions we need here can't be done with one atomic
596 // OR/AND/XOR on `Status.cancelation`, so we don't actually have any other option.
597 const status = thread.status.load(.monotonic);
598 switch (status.cancelation) {
599 .parked => unreachable,
600 .blocked => unreachable,
601 .blocked_windows_dns => unreachable,
602 .blocked_canceling => unreachable,
603 .none, .canceled => {},
604 .canceling => {
605 thread.status.store(.{
606 .cancelation = .canceled,
607 .awaitable = status.awaitable,
608 }, .monotonic);
609 return error.Canceled;
610 },
611 }
254612 }
255613
256 fn currentSignalId() SignaleeId {
614 fn currentSignaleeId() SignaleeId {
257615 return if (std.Thread.use_pthreads) std.c.pthread_self() else std.Thread.getCurrentId();
258616 }
259617
......@@ -262,10 +620,7 @@ const Thread = struct {
262620 }
263621
264622 fn futexWait(thread: *Thread, ptr: *const u32, expect: u32) Io.Cancelable!void {
265 return Thread.futexWaitTimed(thread, ptr, expect, null) catch |err| switch (err) {
266 error.Canceled => return error.Canceled,
267 error.Timeout => unreachable,
268 };
623 return Thread.futexWaitTimed(thread, ptr, expect, null);
269624 }
270625
271626 fn futexWaitTimed(thread: ?*Thread, ptr: *const u32, expect: u32, timeout_ns: ?u64) Io.Cancelable!void {
......@@ -543,123 +898,200 @@ const Thread = struct {
543898 },
544899 }
545900 }
546};
547
548const max_iovecs_len = 8;
549const splat_buffer_size = 64;
550
551comptime {
552 if (@TypeOf(posix.IOV_MAX) != void) assert(max_iovecs_len <= posix.IOV_MAX);
553}
554
555const CancelStatus = enum(usize) {
556 /// Cancellation has neither been requested, nor checked. The async
557 /// operation will check status before entering a blocking syscall.
558 /// This is also the status used for uninteruptible tasks.
559 none = 0,
560 /// Cancellation has been requested and the status will be checked before
561 /// entering a blocking syscall.
562 requested = std.math.maxInt(usize) - 1,
563 /// Cancellation has been acknowledged and is in progress. Signals should
564 /// not be sent.
565 acknowledged = std.math.maxInt(usize),
566 /// Stores a `Thread.SignaleeId` and indicates that sending a signal to this thread
567 /// is needed in order to cancel. This state is set before going into
568 /// a blocking operation that needs to get unblocked via signal.
569 _,
570901
571 const Unpacked = union(enum) {
572 none,
573 requested,
574 acknowledged,
575 signal_id: Thread.SignaleeId,
576 };
902 /// Cancels `thread` if it is working on `awaitable`.
903 ///
904 /// It is possible that `thread` gets canceled by this function, but is blocked in a syscall. In
905 /// that case, the thread may need to be sent a signal to interrupt the call. This function will
906 /// return `true` to indicate this, in which case the caller must call `signalCanceledSyscall`.
907 fn cancelAwaitable(thread: *Thread, awaitable: AwaitableId) bool {
908 var status = thread.status.load(.monotonic);
909 while (true) {
910 if (status.awaitable != awaitable) return false; // thread is working on something else
911 status = switch (status.cancelation) {
912 .none => thread.status.cmpxchgWeak(
913 .{ .cancelation = .none, .awaitable = awaitable },
914 .{ .cancelation = .canceling, .awaitable = awaitable },
915 .monotonic,
916 .monotonic,
917 ) orelse return false,
918
919 .parked => thread.status.cmpxchgWeak(
920 .{ .cancelation = .parked, .awaitable = awaitable },
921 .{ .cancelation = .canceling, .awaitable = awaitable },
922 .monotonic,
923 .monotonic,
924 ) orelse {
925 if (true) @panic("MLUGG TODO: unpark thread");
926 return false;
927 },
577928
578 fn unpack(cs: CancelStatus) Unpacked {
579 return switch (cs) {
580 .none => .none,
581 .requested => .requested,
582 .acknowledged => .acknowledged,
583 _ => |signal_id| .{
584 .signal_id = if (std.Thread.use_pthreads)
585 @ptrFromInt(@intFromEnum(signal_id))
586 else
587 @truncate(@intFromEnum(signal_id)),
588 },
589 };
590 }
929 .blocked => thread.status.cmpxchgWeak(
930 .{ .cancelation = .blocked, .awaitable = awaitable },
931 .{ .cancelation = .blocked_canceling, .awaitable = awaitable },
932 .monotonic,
933 .monotonic,
934 ) orelse return true,
935
936 .blocked_windows_dns => thread.status.cmpxchgWeak(
937 .{ .cancelation = .blocked_windows_dns, .awaitable = awaitable },
938 .{ .cancelation = .canceling, .awaitable = awaitable },
939 .monotonic,
940 .monotonic,
941 ) orelse return false,
942
943 .canceling, .canceled => {
944 // This can happen when the task start raced with the cancelation, so the thread
945 // saw the cancelation on the future/group *and* we are trying to signal the
946 // thread here.
947 return false;
948 },
591949
592 fn fromSignaleeId(signal_id: Thread.SignaleeId) CancelStatus {
593 return if (std.Thread.use_pthreads)
594 @enumFromInt(@intFromPtr(signal_id))
595 else
596 @enumFromInt(signal_id);
950 .blocked_canceling => unreachable,
951 };
952 }
597953 }
598};
599954
600const Closure = struct {
601 start: Start,
602 node: std.SinglyLinkedList.Node = .{},
603 cancel_status: CancelStatus,
604
605 const Start = *const fn (*Closure, *Threaded) void;
955 /// Sends a signal to `thread` if it is still blocked in a syscall (i.e. has not yet observed
956 /// the cancelation request from `cancelAwaitable`).
957 ///
958 /// Unfortunately, the signal could arrive before the syscall actually starts, so the interrupt
959 /// is missed. To handle this, we may need to send multiple signals. As such, if this function
960 /// returns `true`, then it should be called again after a short delay to send another signal if
961 /// the thread is still blocked. For the implementation, `Future.waitForCancelWithSignaling` and
962 /// `Group.waitForCancelWithSignaling`: they use exponential backoff starting at a 1us delay and
963 /// doubling each call. In practice, it is rare to send more than one signal.
964 fn signalCanceledSyscall(thread: *Thread, t: *Threaded, awaitable: AwaitableId) bool {
965 const bad_status: Status = .{ .cancelation = .blocked_canceling, .awaitable = awaitable };
966 if (thread.status.load(.monotonic) != bad_status) return false;
967
968 // The thread ID can be read non-atomically because it never changes and was released by the
969 // store that made `thread` available to us.
970 const signalee_id = thread.signalee_id;
971
972 if (std.Thread.use_pthreads) {
973 if (std.c.pthread_kill(signalee_id, .IO) != 0) return false;
974 } else if (native_os == .linux) {
975 const pid: posix.pid_t = pid: {
976 const cached_pid = @atomicLoad(Pid, &t.pid, .monotonic);
977 if (cached_pid != .unknown) break :pid @intFromEnum(cached_pid);
978 const pid = std.os.linux.getpid();
979 @atomicStore(Pid, &t.pid, @enumFromInt(pid), .monotonic);
980 break :pid pid;
981 };
982 if (std.os.linux.tgkill(pid, @bitCast(signalee_id), .IO) != 0) return false;
983 } else {
984 @compileError("MLUGG TODO");
985 }
606986
607 fn requestCancel(closure: *Closure, t: *Threaded) void {
608 var signal_id = switch (@atomicRmw(CancelStatus, &closure.cancel_status, .Xchg, .requested, .monotonic).unpack()) {
609 .none, .acknowledged, .requested => return,
610 .signal_id => |signal_id| signal_id,
611 };
612 // The task will enter a blocking syscall before checking for cancellation again.
613 // We can send a signal to interrupt the syscall, but if it arrives before
614 // the syscall instruction, it will be missed. Therefore, this code tries
615 // again until the cancellation request is acknowledged.
616
617 // 1 << 10 ns is about 1 microsecond, approximately syscall overhead.
618 // 1 << 20 ns is about 1 millisecond.
619 // 1 << 30 ns is about 1 second.
620 //
621 // On a heavily loaded Linux 6.17.5, I observed a maximum of 20
622 // attempts not acknowledged before the timeout (including exponential
623 // backoff) was sufficient, despite the heavy load.
624 const max_attempts = 22;
625
626 for (0..max_attempts) |attempt_index| {
627 if (std.Thread.use_pthreads) {
628 if (std.c.pthread_kill(signal_id, .IO) != 0) return;
629 } else if (native_os == .linux) {
630 const pid: posix.pid_t = p: {
631 const cached_pid = @atomicLoad(Pid, &t.pid, .monotonic);
632 if (cached_pid != .unknown) break :p @intFromEnum(cached_pid);
633 const pid = std.os.linux.getpid();
634 @atomicStore(Pid, &t.pid, @enumFromInt(pid), .monotonic);
635 break :p pid;
636 };
637 if (std.os.linux.tgkill(pid, @bitCast(signal_id), .IO) != 0) return;
638 } else {
639 return;
640 }
987 return true;
988 }
641989
642 if (t.robust_cancel != .enabled) return;
990 /// Like a `*Thread`, but 2 bits smaller than a pointer (because the LSBs are always 0 due to
991 /// alignment) so that those two bits can be used in a `packed struct`.
992 const PackedPtr = enum(@Int(.unsigned, @bitSizeOf(usize) - 2)) {
993 null = 0,
994 all_ones = std.math.maxInt(@Int(.unsigned, @bitSizeOf(usize) - 2)),
995 _,
643996
644 var timespec: posix.timespec = .{
645 .sec = 0,
646 .nsec = @as(isize, 1) << @intCast(attempt_index),
647 };
648 if (native_os == .linux) {
649 _ = std.os.linux.clock_nanosleep(posix.CLOCK.MONOTONIC, .{ .ABSTIME = false }, &timespec, &timespec);
650 } else {
651 _ = posix.system.nanosleep(&timespec, &timespec);
652 }
997 const Split = packed struct(usize) { low: u2, high: PackedPtr };
998 fn pack(ptr: *Thread) PackedPtr {
999 const split: Split = @bitCast(@intFromPtr(ptr));
1000 assert(split.low == 0);
1001 return split.high;
1002 }
1003 fn unpack(ptr: PackedPtr) ?*Thread {
1004 const split: Split = .{ .low = 0, .high = ptr };
1005 return @ptrFromInt(@as(usize, @bitCast(split)));
1006 }
1007 };
1008};
6531009
654 switch (@atomicRmw(CancelStatus, &closure.cancel_status, .Xchg, .requested, .monotonic).unpack()) {
655 .requested => continue, // Retry needed in case other thread hasn't yet entered the syscall.
656 .none, .acknowledged => return,
657 .signal_id => |new_signal_id| signal_id = new_signal_id,
658 }
1010const Syscall = struct {
1011 thread: ?*Thread,
1012 /// Marks entry to a syscall region. This should be tightly scoped around the actual syscall
1013 /// to minimize races. The syscall must be marked as "finished" by `checkCancel`, `finish`,
1014 /// or one of the wrappers of `finish`.
1015 fn start() Io.Cancelable!Syscall {
1016 const thread = Thread.current orelse return .{ .thread = null };
1017 switch (thread.cancel_protection) {
1018 .blocked => return .{ .thread = null },
1019 .unblocked => {},
1020 }
1021 switch (thread.status.fetchOr(.{
1022 .cancelation = @enumFromInt(0b011),
1023 .awaitable = .null,
1024 }, .monotonic).cancelation) {
1025 .parked => unreachable,
1026 .blocked => unreachable,
1027 .blocked_windows_dns => unreachable,
1028 .blocked_canceling => unreachable,
1029 .none => return .{ .thread = thread }, // new status is `.blocked`
1030 .canceling => return error.Canceled, // new status is `.canceled`
1031 .canceled => return .{ .thread = null }, // new status is `.canceled` (unchanged)
1032 }
1033 }
1034 /// Checks whether this syscall has been canceled. This should be called when a syscall is
1035 /// interrupted through a mechanism which may indicate cancelation, or may be spurious. If
1036 /// the syscall was canceled, it is finished and `error.Canceled` is returned. Otherwise,
1037 /// the syscall is not marked finished, and the caller should retry.
1038 fn checkCancel(s: Syscall) Io.Cancelable!void {
1039 const thread = s.thread orelse return;
1040 switch (thread.status.fetchOr(.{
1041 .cancelation = @enumFromInt(0b010),
1042 .awaitable = .null,
1043 }, .monotonic).cancelation) {
1044 .none => unreachable,
1045 .parked => unreachable,
1046 .blocked_windows_dns => unreachable,
1047 .canceling => unreachable,
1048 .canceled => unreachable,
1049 .blocked => {}, // new status is `.blocked` (unchanged)
1050 .blocked_canceling => return error.Canceled, // new status is `.canceled`
1051 }
1052 }
1053 /// Marks this syscall as finished.
1054 fn finish(s: Syscall) void {
1055 const thread = s.thread orelse return;
1056 switch (thread.status.fetchXor(.{
1057 .cancelation = @enumFromInt(0b011),
1058 .awaitable = .null,
1059 }, .monotonic).cancelation) {
1060 .none => unreachable,
1061 .parked => unreachable,
1062 .blocked_windows_dns => unreachable,
1063 .canceling => unreachable,
1064 .canceled => unreachable,
1065 .blocked => {}, // new status is `.none`
1066 .blocked_canceling => {}, // new status is `.canceling`
6591067 }
6601068 }
1069 /// Convenience wrapper which calls `finish`, then returns `err`.
1070 fn fail(s: Syscall, err: anytype) @TypeOf(err) {
1071 s.finish();
1072 return err;
1073 }
1074 /// Convenience wrapper which calls `finish`, then calls `Threaded.errnoBug`.
1075 fn errnoBug(s: Syscall, err: posix.E) Io.UnexpectedError {
1076 @branchHint(.cold);
1077 s.finish();
1078 return Threaded.errnoBug(err);
1079 }
1080 /// Convenience wrapper which calls `finish`, then calls `posix.unexpectedErrno`.
1081 fn unexpectedErrno(s: Syscall, err: posix.E) Io.UnexpectedError {
1082 @branchHint(.cold);
1083 s.finish();
1084 return posix.unexpectedErrno(err);
1085 }
6611086};
6621087
1088const max_iovecs_len = 8;
1089const splat_buffer_size = 64;
1090
1091comptime {
1092 if (@TypeOf(posix.IOV_MAX) != void) assert(max_iovecs_len <= posix.IOV_MAX);
1093}
1094
6631095pub const InitOptions = struct {
6641096 /// Affects how many bytes are memory-mapped for threads.
6651097 stack_size: usize = std.Thread.SpawnConfig.default_stack_size,
......@@ -727,14 +1159,10 @@ pub fn init(
7271159 .old_sig_io = undefined,
7281160 .old_sig_pipe = undefined,
7291161 .have_signal_handler = false,
730 .main_thread = .{
731 .signal_id = Thread.currentSignalId(),
732 .current_closure = null,
733 .cancel_protection = .unblocked,
734 },
7351162 .argv0 = options.argv0,
7361163 .environ = options.environ,
7371164 .robust_cancel = options.robust_cancel,
1165 .worker_threads = .init(null),
7381166 };
7391167
7401168 if (posix.Sigaction != void) {
......@@ -768,14 +1196,10 @@ pub const init_single_threaded: Threaded = .{
7681196 .old_sig_io = undefined,
7691197 .old_sig_pipe = undefined,
7701198 .have_signal_handler = false,
771 .main_thread = .{
772 .signal_id = undefined,
773 .current_closure = null,
774 .cancel_protection = .unblocked,
775 },
7761199 .robust_cancel = .disabled,
7771200 .argv0 = .{},
7781201 .environ = .{},
1202 .worker_threads = .init(null),
7791203};
7801204
7811205var global_single_threaded_instance: Threaded = .init_single_threaded;
......@@ -822,22 +1246,40 @@ fn join(t: *Threaded) void {
8221246
8231247fn worker(t: *Threaded) void {
8241248 var thread: Thread = .{
825 .signal_id = Thread.currentSignalId(),
826 .current_closure = null,
1249 .next = undefined,
1250 .signalee_id = Thread.currentSignaleeId(),
1251 .status = .init(.{
1252 .cancelation = .none,
1253 .awaitable = .null,
1254 }),
8271255 .cancel_protection = .unblocked,
8281256 };
8291257 Thread.current = &thread;
8301258
1259 {
1260 var head = t.worker_threads.load(.monotonic);
1261 while (true) {
1262 thread.next = head;
1263 head = t.worker_threads.cmpxchgWeak(
1264 head,
1265 &thread,
1266 .release,
1267 .monotonic,
1268 ) orelse break;
1269 }
1270 }
1271
8311272 defer t.wait_group.finish();
8321273
8331274 t.mutex.lock();
8341275 defer t.mutex.unlock();
8351276
8361277 while (true) {
837 while (t.run_queue.popFirst()) |closure_node| {
1278 while (t.run_queue.popFirst()) |runnable_node| {
8381279 t.mutex.unlock();
839 const closure: *Closure = @fieldParentPtr("node", closure_node);
840 closure.start(closure, t);
1280 thread.cancel_protection = .unblocked;
1281 const runnable: *Runnable = @fieldParentPtr("node", runnable_node);
1282 runnable.startFn(runnable, &thread, t);
8411283 t.mutex.lock();
8421284 t.busy_count -= 1;
8431285 }
......@@ -1145,103 +1587,6 @@ const linux_copy_file_range_use_c = std.c.versionCheck(if (builtin.abi.isAndroid
11451587});
11461588const linux_copy_file_range_sys = if (linux_copy_file_range_use_c) std.c else std.os.linux;
11471589
1148/// Trailing data:
1149/// 1. context
1150/// 2. result
1151const AsyncClosure = struct {
1152 closure: Closure,
1153 func: *const fn (context: *anyopaque, result: *anyopaque) void,
1154 event: Io.Event,
1155 select_condition: ?*Io.Event,
1156 context_alignment: Alignment,
1157 result_offset: usize,
1158 alloc_len: usize,
1159
1160 const done_event: *Io.Event = @ptrFromInt(@alignOf(Io.Event));
1161
1162 fn start(closure: *Closure, t: *Threaded) void {
1163 const ac: *AsyncClosure = @alignCast(@fieldParentPtr("closure", closure));
1164 const current_thread = Thread.getCurrent(t);
1165
1166 current_thread.current_closure = closure;
1167 current_thread.cancel_protection = .unblocked;
1168
1169 ac.func(ac.contextPointer(), ac.resultPointer());
1170
1171 current_thread.current_closure = null;
1172 current_thread.cancel_protection = undefined;
1173
1174 if (@atomicRmw(?*Io.Event, &ac.select_condition, .Xchg, done_event, .release)) |select_event| {
1175 assert(select_event != done_event);
1176 select_event.set(ioBasic(t));
1177 }
1178 ac.event.set(ioBasic(t));
1179 }
1180
1181 fn resultPointer(ac: *AsyncClosure) [*]u8 {
1182 const base: [*]u8 = @ptrCast(ac);
1183 return base + ac.result_offset;
1184 }
1185
1186 fn contextPointer(ac: *AsyncClosure) [*]u8 {
1187 const base: [*]u8 = @ptrCast(ac);
1188 const context_offset = ac.context_alignment.forward(@intFromPtr(ac) + @sizeOf(AsyncClosure)) - @intFromPtr(ac);
1189 return base + context_offset;
1190 }
1191
1192 fn init(
1193 gpa: Allocator,
1194 result_len: usize,
1195 result_alignment: Alignment,
1196 context: []const u8,
1197 context_alignment: Alignment,
1198 func: *const fn (context: *const anyopaque, result: *anyopaque) void,
1199 ) Allocator.Error!*AsyncClosure {
1200 const max_context_misalignment = context_alignment.toByteUnits() -| @alignOf(AsyncClosure);
1201 const worst_case_context_offset = context_alignment.forward(@sizeOf(AsyncClosure) + max_context_misalignment);
1202 const worst_case_result_offset = result_alignment.forward(worst_case_context_offset + context.len);
1203 const alloc_len = worst_case_result_offset + result_len;
1204
1205 const ac: *AsyncClosure = @ptrCast(@alignCast(try gpa.alignedAlloc(u8, .of(AsyncClosure), alloc_len)));
1206 errdefer comptime unreachable;
1207
1208 const actual_context_addr = context_alignment.forward(@intFromPtr(ac) + @sizeOf(AsyncClosure));
1209 const actual_result_addr = result_alignment.forward(actual_context_addr + context.len);
1210 const actual_result_offset = actual_result_addr - @intFromPtr(ac);
1211 ac.* = .{
1212 .closure = .{
1213 .cancel_status = .none,
1214 .start = start,
1215 },
1216 .func = func,
1217 .context_alignment = context_alignment,
1218 .result_offset = actual_result_offset,
1219 .alloc_len = alloc_len,
1220 .event = .unset,
1221 .select_condition = null,
1222 };
1223 @memcpy(ac.contextPointer()[0..context.len], context);
1224 return ac;
1225 }
1226
1227 fn waitAndDeinit(ac: *AsyncClosure, t: *Threaded, result: []u8) void {
1228 ac.event.wait(ioBasic(t)) catch |err| switch (err) {
1229 error.Canceled => {
1230 ac.closure.requestCancel(t);
1231 ac.event.waitUncancelable(ioBasic(t));
1232 recancel(t);
1233 },
1234 };
1235 @memcpy(result, ac.resultPointer()[0..result.len]);
1236 ac.deinit(t.allocator);
1237 }
1238
1239 fn deinit(ac: *AsyncClosure, gpa: Allocator) void {
1240 const base: [*]align(@alignOf(AsyncClosure)) u8 = @ptrCast(ac);
1241 gpa.free(base[0..ac.alloc_len]);
1242 }
1243};
1244
12451590fn async(
12461591 userdata: ?*anyopaque,
12471592 result: []u8,
......@@ -1255,10 +1600,13 @@ fn async(
12551600 start(context.ptr, result.ptr);
12561601 return null;
12571602 }
1603
12581604 const gpa = t.allocator;
1259 const ac = AsyncClosure.init(gpa, result.len, result_alignment, context, context_alignment, start) catch {
1260 start(context.ptr, result.ptr);
1261 return null;
1605 const future = Future.create(gpa, result.len, result_alignment, context, context_alignment, start) catch |err| switch (err) {
1606 error.OutOfMemory => {
1607 start(context.ptr, result.ptr);
1608 return null;
1609 },
12621610 };
12631611
12641612 t.mutex.lock();
......@@ -1267,7 +1615,7 @@ fn async(
12671615
12681616 if (busy_count >= @intFromEnum(t.async_limit)) {
12691617 t.mutex.unlock();
1270 ac.deinit(gpa);
1618 future.destroy(gpa);
12711619 start(context.ptr, result.ptr);
12721620 return null;
12731621 }
......@@ -1281,17 +1629,18 @@ fn async(
12811629 t.wait_group.finish();
12821630 t.busy_count = busy_count;
12831631 t.mutex.unlock();
1284 ac.deinit(gpa);
1632 future.destroy(gpa);
12851633 start(context.ptr, result.ptr);
12861634 return null;
12871635 };
12881636 thread.detach();
12891637 }
12901638
1291 t.run_queue.prepend(&ac.closure.node);
1639 t.run_queue.prepend(&future.runnable.node);
1640
12921641 t.mutex.unlock();
12931642 t.cond.signal();
1294 return @ptrCast(ac);
1643 return @ptrCast(future);
12951644}
12961645
12971646fn concurrent(
......@@ -1307,9 +1656,10 @@ fn concurrent(
13071656 const t: *Threaded = @ptrCast(@alignCast(userdata));
13081657
13091658 const gpa = t.allocator;
1310 const ac = AsyncClosure.init(gpa, result_len, result_alignment, context, context_alignment, start) catch
1311 return error.ConcurrencyUnavailable;
1312 errdefer ac.deinit(gpa);
1659 const future = Future.create(gpa, result_len, result_alignment, context, context_alignment, start) catch |err| switch (err) {
1660 error.OutOfMemory => return error.ConcurrencyUnavailable,
1661 };
1662 errdefer future.destroy(gpa);
13131663
13141664 t.mutex.lock();
13151665 defer t.mutex.unlock();
......@@ -1329,110 +1679,32 @@ fn concurrent(
13291679
13301680 const thread = std.Thread.spawn(.{ .stack_size = t.stack_size }, worker, .{t}) catch
13311681 return error.ConcurrencyUnavailable;
1682
13321683 thread.detach();
13331684 }
13341685
1335 t.run_queue.prepend(&ac.closure.node);
1686 t.run_queue.prepend(&future.runnable.node);
1687
13361688 t.cond.signal();
1337 return @ptrCast(ac);
1689 return @ptrCast(future);
13381690}
13391691
1340const GroupClosure = struct {
1341 closure: Closure,
1342 group: *Io.Group,
1343 /// Points to sibling `GroupClosure`. Used for walking the group to cancel all.
1344 node: std.SinglyLinkedList.Node,
1345 func: *const fn (*Io.Group, context: *anyopaque) Io.Cancelable!void,
1346 context_alignment: Alignment,
1347 alloc_len: usize,
1348
1349 fn start(closure: *Closure, t: *Threaded) void {
1350 const gc: *GroupClosure = @alignCast(@fieldParentPtr("closure", closure));
1351 const current_thread = Thread.getCurrent(t);
1352 const group = gc.group;
1353 const group_state: *std.atomic.Value(usize) = @ptrCast(&group.state);
1354 const event: *Io.Event = @ptrCast(&group.context);
1355 current_thread.current_closure = closure;
1356 current_thread.cancel_protection = .unblocked;
1357
1358 assertResult(closure, gc.func(group, gc.contextPointer()));
1359
1360 current_thread.current_closure = null;
1361 current_thread.cancel_protection = undefined;
1362
1363 const prev_state = group_state.fetchSub(sync_one_pending, .acq_rel);
1364 assert((prev_state / sync_one_pending) > 0);
1365 if (prev_state == (sync_one_pending | sync_is_waiting)) event.set(ioBasic(t));
1366 }
1367
1368 fn assertResult(closure: *Closure, result: Io.Cancelable!void) void {
1369 if (result) |_| switch (closure.cancel_status.unpack()) {
1370 .none, .requested => {},
1371 .acknowledged => unreachable, // task illegally swallowed error.Canceled
1372 .signal_id => unreachable,
1373 } else |err| switch (err) {
1374 error.Canceled => assert(closure.cancel_status == .acknowledged),
1375 }
1376 }
1377
1378 fn contextPointer(gc: *GroupClosure) [*]u8 {
1379 const base: [*]u8 = @ptrCast(gc);
1380 const context_offset = gc.context_alignment.forward(@intFromPtr(gc) + @sizeOf(GroupClosure)) - @intFromPtr(gc);
1381 return base + context_offset;
1382 }
1383
1384 /// Does not initialize the `node` field.
1385 fn init(
1386 gpa: Allocator,
1387 group: *Io.Group,
1388 context: []const u8,
1389 context_alignment: Alignment,
1390 func: *const fn (*Io.Group, context: *const anyopaque) Io.Cancelable!void,
1391 ) Allocator.Error!*GroupClosure {
1392 const max_context_misalignment = context_alignment.toByteUnits() -| @alignOf(GroupClosure);
1393 const worst_case_context_offset = context_alignment.forward(@sizeOf(GroupClosure) + max_context_misalignment);
1394 const alloc_len = worst_case_context_offset + context.len;
1395
1396 const gc: *GroupClosure = @ptrCast(@alignCast(try gpa.alignedAlloc(u8, .of(GroupClosure), alloc_len)));
1397 errdefer comptime unreachable;
1398
1399 gc.* = .{
1400 .closure = .{
1401 .cancel_status = .none,
1402 .start = start,
1403 },
1404 .group = group,
1405 .node = undefined,
1406 .func = func,
1407 .context_alignment = context_alignment,
1408 .alloc_len = alloc_len,
1409 };
1410 @memcpy(gc.contextPointer()[0..context.len], context);
1411 return gc;
1412 }
1413
1414 fn deinit(gc: *GroupClosure, gpa: Allocator) void {
1415 const base: [*]align(@alignOf(GroupClosure)) u8 = @ptrCast(gc);
1416 gpa.free(base[0..gc.alloc_len]);
1417 }
1418
1419 const sync_is_waiting: usize = 1 << 0;
1420 const sync_one_pending: usize = 1 << 1;
1421};
1422
14231692fn groupAsync(
14241693 userdata: ?*anyopaque,
1425 group: *Io.Group,
1694 type_erased: *Io.Group,
14261695 context: []const u8,
14271696 context_alignment: Alignment,
14281697 start: *const fn (*Io.Group, context: *const anyopaque) Io.Cancelable!void,
14291698) void {
14301699 const t: *Threaded = @ptrCast(@alignCast(userdata));
1431 if (builtin.single_threaded) return start(group, context.ptr) catch unreachable;
1700 const g: Group = .{ .ptr = type_erased };
1701
1702 if (builtin.single_threaded) return start(g.ptr, context.ptr) catch unreachable;
14321703
14331704 const gpa = t.allocator;
1434 const gc = GroupClosure.init(gpa, group, context, context_alignment, start) catch
1435 return t.assertGroupResult(start(group, context.ptr));
1705 const task = Group.Task.create(gpa, g, context, context_alignment, start) catch |err| switch (err) {
1706 error.OutOfMemory => return t.assertGroupResult(start(g.ptr, context.ptr)),
1707 };
14361708
14371709 t.mutex.lock();
14381710
......@@ -1440,8 +1712,8 @@ fn groupAsync(
14401712
14411713 if (busy_count >= @intFromEnum(t.async_limit)) {
14421714 t.mutex.unlock();
1443 gc.deinit(gpa);
1444 return t.assertGroupResult(start(group, context.ptr));
1715 task.destroy(gpa);
1716 return t.assertGroupResult(start(g.ptr, context.ptr));
14451717 }
14461718
14471719 t.busy_count = busy_count + 1;
......@@ -1453,37 +1725,48 @@ fn groupAsync(
14531725 t.wait_group.finish();
14541726 t.busy_count = busy_count;
14551727 t.mutex.unlock();
1456 gc.deinit(gpa);
1457 return t.assertGroupResult(start(group, context.ptr));
1728 task.destroy(gpa);
1729 return t.assertGroupResult(start(g.ptr, context.ptr));
14581730 };
14591731 thread.detach();
14601732 }
14611733
1462 // Append to the group linked list inside the mutex to make `Io.Group.async` thread-safe.
1463 gc.node = .{ .next = @ptrCast(@alignCast(group.token.load(.monotonic))) };
1464 group.token.store(&gc.node, .monotonic);
1465
1466 t.run_queue.prepend(&gc.closure.node);
1467
1468 // This needs to be done before unlocking the mutex to avoid a race with
1469 // the associated task finishing.
1470 const group_state: *std.atomic.Value(usize) = @ptrCast(&group.state);
1471 const prev_state = group_state.fetchAdd(GroupClosure.sync_one_pending, .monotonic);
1472 assert((prev_state / GroupClosure.sync_one_pending) < (std.math.maxInt(usize) / GroupClosure.sync_one_pending));
1734 // TODO: if this logic is changed to be lock-free, this `fetchAdd` must be released by the queue
1735 // prepend so that the task doesn't finish without observing this and try to decrement the count
1736 // below zero.
1737 _ = g.status().fetchAdd(.{
1738 .num_running = 1,
1739 .have_awaiter = false,
1740 .canceled = false,
1741 }, .monotonic);
1742 t.run_queue.prepend(&task.runnable.node);
14731743
14741744 t.mutex.unlock();
14751745 t.cond.signal();
14761746}
14771747
1478fn assertGroupResult(t: *Threaded, result: Io.Cancelable!void) void {
1479 const current_thread: *Thread = .getCurrent(t);
1480 const current_closure = current_thread.current_closure orelse return;
1481 GroupClosure.assertResult(current_closure, result);
1748fn assertGroupResult(result: Io.Cancelable!void) void {
1749 const cancel_acknowledged = if (Thread.current) |thread|
1750 switch (thread.status.load(.monotonic).cancelation) {
1751 .none, .canceling => false,
1752 .canceled => true,
1753 .parked => unreachable,
1754 .blocked => unreachable,
1755 .blocked_windows_dns => unreachable,
1756 .blocked_canceling => unreachable,
1757 }
1758 else
1759 false;
1760 if (result) {
1761 assert(!cancel_acknowledged); // group task acknowledged cancelation but did not return `error.Canceled`
1762 } else |err| switch (err) {
1763 error.Canceled => assert(cancel_acknowledged), // group task returned `error.Canceled` but was never canceled
1764 }
14821765}
14831766
14841767fn groupConcurrent(
14851768 userdata: ?*anyopaque,
1486 group: *Io.Group,
1769 type_erased: *Io.Group,
14871770 context: []const u8,
14881771 context_alignment: Alignment,
14891772 start: *const fn (*Io.Group, context: *const anyopaque) Io.Cancelable!void,
......@@ -1491,10 +1774,13 @@ fn groupConcurrent(
14911774 if (builtin.single_threaded) return error.ConcurrencyUnavailable;
14921775
14931776 const t: *Threaded = @ptrCast(@alignCast(userdata));
1777 const g: Group = .{ .ptr = type_erased };
14941778
14951779 const gpa = t.allocator;
1496 const gc = GroupClosure.init(gpa, group, context, context_alignment, start) catch
1497 return error.ConcurrencyUnavailable;
1780 const task = Group.Task.create(gpa, g, context, context_alignment, start) catch |err| switch (err) {
1781 error.OutOfMemory => return error.ConcurrencyUnavailable,
1782 };
1783 errdefer task.destroy(gpa);
14981784
14991785 t.mutex.lock();
15001786 defer t.mutex.unlock();
......@@ -1514,102 +1800,126 @@ fn groupConcurrent(
15141800
15151801 const thread = std.Thread.spawn(.{ .stack_size = t.stack_size }, worker, .{t}) catch
15161802 return error.ConcurrencyUnavailable;
1803
15171804 thread.detach();
15181805 }
15191806
1520 // Append to the group linked list inside the mutex to make `Io.Group.concurrent` thread-safe.
1521 gc.node = .{ .next = @ptrCast(@alignCast(group.token.load(.monotonic))) };
1522 group.token.store(&gc.node, .monotonic);
1523
1524 t.run_queue.prepend(&gc.closure.node);
1525
1526 // This needs to be done before unlocking the mutex to avoid a race with
1527 // the associated task finishing.
1528 const group_state: *std.atomic.Value(usize) = @ptrCast(&group.state);
1529 const prev_state = group_state.fetchAdd(GroupClosure.sync_one_pending, .monotonic);
1530 assert((prev_state / GroupClosure.sync_one_pending) < (std.math.maxInt(usize) / GroupClosure.sync_one_pending));
1807 // TODO: if this logic is changed to be lock-free, this `fetchAdd` must be released by the queue
1808 // prepend so that the task doesn't finish without observing this and try to decrement the count
1809 // below zero.
1810 _ = g.status().fetchAdd(.{
1811 .num_running = 1,
1812 .have_awaiter = false,
1813 .canceled = false,
1814 }, .monotonic);
1815 t.run_queue.prepend(&task.runnable.node);
15311816
15321817 t.cond.signal();
15331818}
15341819
1535fn groupAwait(userdata: ?*anyopaque, group: *Io.Group, initial_token: *anyopaque) Io.Cancelable!void {
1820fn groupAwait(userdata: ?*anyopaque, type_erased: *Io.Group, initial_token: *anyopaque) Io.Cancelable!void {
1821 _ = initial_token; // we need to load `token` *after* the group finishes
15361822 const t: *Threaded = @ptrCast(@alignCast(userdata));
1537 const gpa = t.allocator;
1823 const g: Group = .{ .ptr = type_erased };
1824 const thread: *Thread = .getCurrent(t);
15381825
1539 _ = initial_token; // we need to load `token` *after* the group finishes
1826 var num_completed: std.atomic.Value(u32) = .init(0);
1827 g.awaiter().* = &num_completed;
15401828
1541 if (builtin.single_threaded) unreachable; // we never set `group.token` to non-`null`
1829 const pre_await_status = g.status().fetchOr(.{
1830 .num_running = 0,
1831 .have_awaiter = true,
1832 .canceled = false,
1833 }, .acq_rel); // acquire results if complete; release `g.awaiter()`
15421834
1543 const group_state: *std.atomic.Value(usize) = @ptrCast(&group.state);
1544 const event: *Io.Event = @ptrCast(&group.context);
1545 const prev_state = group_state.fetchAdd(GroupClosure.sync_is_waiting, .acquire);
1546 assert(prev_state & GroupClosure.sync_is_waiting == 0);
1547 {
1548 errdefer _ = group_state.fetchSub(GroupClosure.sync_is_waiting, .monotonic);
1549 // This event.wait can return error.Canceled, in which case this logic does
1550 // *not* propagate cancel requests to each group member. Instead, the user
1551 // code will likely do this with a defered call to groupCancel, or,
1552 // intentionally not do this.
1553 if ((prev_state / GroupClosure.sync_one_pending) > 0) try event.wait(ioBasic(t));
1835 assert(!pre_await_status.have_awaiter);
1836 assert(!pre_await_status.canceled);
1837 if (pre_await_status.num_running == 0) {
1838 // Already done. Since the group is finished, it's illegal to spawn more tasks in it
1839 // until we return, so we can access `g.status()` non-atomically.
1840 g.status().raw.have_awaiter = false;
1841 return;
15541842 }
15551843
1556 // Since the group has now finished, it's illegal to add more tasks to it until we return. It's
1557 // also illegal for us to race with another `await` or `cancel`. Therefore, we must be the only
1558 // thread who can access `group` right now.
1559 var it: ?*std.SinglyLinkedList.Node = @ptrCast(@alignCast(group.token.raw));
1560 group.token.raw = null;
1561 while (it) |node| {
1562 it = node.next; // update `it` now, because `deinit` will invalidate `node`
1563 const gc: *GroupClosure = @fieldParentPtr("node", node);
1564 gc.deinit(gpa);
1844 while (thread.futexWait(&num_completed.raw, 0)) {
1845 switch (num_completed.load(.acquire)) { // acquire task results
1846 0 => continue,
1847 1 => break,
1848 else => unreachable, // group was reused before `await` returned
1849 }
1850 } else |err| switch (err) {
1851 error.Canceled => {
1852 const pre_cancel_status = g.status().fetchOr(.{
1853 .num_running = 0,
1854 .have_awaiter = false,
1855 .canceled = true,
1856 }, .acq_rel); // acquire results if complete; release `g.awaiter()`
1857 assert(pre_cancel_status.have_awaiter);
1858 assert(!pre_cancel_status.canceled);
1859
1860 // Even if `pre_cancel_status.num_running == 0`, we still need to wait for the signal,
1861 // because in that case the last member of the group is already trying to modify it.
1862 // However, if we know everything is done, we *can* skip signaling blocked threads.
1863 const skip_signals = pre_cancel_status.num_running == 0;
1864 g.waitForCancelWithSignaling(t, &num_completed, skip_signals);
1865
1866 // The group is finished, so it's illegal to spawn more tasks in it until we return, so
1867 // we can access `g.status()` non-atomically.
1868 g.status().raw.canceled = false;
1869 g.status().raw.have_awaiter = false;
1870 return error.Canceled;
1871 },
15651872 }
1873
1874 // The group is finished, so it's illegal to spawn more tasks in it until we return, so
1875 // we can access `g.status()` non-atomically.
1876 g.status().raw.have_awaiter = false;
15661877}
15671878
1568fn groupCancel(userdata: ?*anyopaque, group: *Io.Group, initial_token: *anyopaque) void {
1879fn groupCancel(userdata: ?*anyopaque, type_erased: *Io.Group, initial_token: *anyopaque) void {
1880 _ = initial_token;
15691881 const t: *Threaded = @ptrCast(@alignCast(userdata));
1570 const gpa = t.allocator;
1882 const g: Group = .{ .ptr = type_erased };
15711883
1572 _ = initial_token; // we need to load `token` *after* the group finishes
1884 var num_completed: std.atomic.Value(u32) = .init(0);
1885 g.awaiter().* = &num_completed;
15731886
1574 if (builtin.single_threaded) unreachable; // we never set `group.token` to non-`null`
1887 const pre_cancel_status = g.status().fetchOr(.{
1888 .num_running = 0,
1889 .have_awaiter = true,
1890 .canceled = true,
1891 }, .acq_rel); // acquire results if complete; release `g.awaiter()`
15751892
1576 {
1577 var it: ?*std.SinglyLinkedList.Node = @ptrCast(@alignCast(group.token.load(.monotonic)));
1578 while (it) |node| : (it = node.next) {
1579 const gc: *GroupClosure = @fieldParentPtr("node", node);
1580 gc.closure.requestCancel(t);
1581 }
1893 assert(!pre_cancel_status.have_awaiter);
1894 assert(!pre_cancel_status.canceled);
1895 if (pre_cancel_status.num_running == 0) {
1896 // Already done. Since the group is finished, it's illegal to spawn more tasks in it
1897 // until we return, so we can access `g.status()` non-atomically.
1898 g.status().raw.have_awaiter = false;
1899 g.status().raw.canceled = false;
1900 return;
15821901 }
15831902
1584 const group_state: *std.atomic.Value(usize) = @ptrCast(&group.state);
1585 const event: *Io.Event = @ptrCast(&group.context);
1586 const prev_state = group_state.fetchAdd(GroupClosure.sync_is_waiting, .acquire);
1587 assert(prev_state & GroupClosure.sync_is_waiting == 0);
1588 if ((prev_state / GroupClosure.sync_one_pending) > 0) event.waitUncancelable(ioBasic(t));
1903 g.waitForCancelWithSignaling(t, &num_completed, false);
15891904
1590 // Since the group has now finished, it's illegal to add more tasks to it until we return. It's
1591 // also illegal for us to race with another `await` or `cancel`. Therefore, we must be the only
1592 // thread who can access `group` right now.
1593 var it: ?*std.SinglyLinkedList.Node = @ptrCast(@alignCast(group.token.raw));
1594 group.token.raw = null;
1595 while (it) |node| {
1596 it = node.next; // update `it` now, because `deinit` will invalidate `node`
1597 const gc: *GroupClosure = @fieldParentPtr("node", node);
1598 gc.deinit(gpa);
1599 }
1905 g.status().raw = .{ .num_running = 0, .have_awaiter = false, .canceled = false };
16001906}
16011907
16021908fn recancel(userdata: ?*anyopaque) void {
16031909 const t: *Threaded = @ptrCast(@alignCast(userdata));
16041910 const current_thread: *Thread = .getCurrent(t);
1605 const cancel_status = &current_thread.current_closure.?.cancel_status;
1606 switch (@atomicLoad(CancelStatus, cancel_status, .monotonic)) {
1607 .none => unreachable, // called `recancel` when not canceled
1608 .requested => unreachable, // called `recancel` when cancelation was already outstanding
1609 .acknowledged => {},
1610 _ => unreachable, // invalid state: not in a syscall
1911 switch (current_thread.status.fetchXor(.{
1912 .cancelation = @enumFromInt(0b001),
1913 .awaitable = .null,
1914 }, .monotonic).cancelation) {
1915 .canceled => {},
1916 .none => unreachable, // called `recancel` but was not canceled
1917 .canceling => unreachable, // called `recancel` but cancelation was already pending
1918 .parked => unreachable,
1919 .blocked => unreachable,
1920 .blocked_windows_dns => unreachable,
1921 .blocked_canceling => unreachable,
16111922 }
1612 @atomicStore(CancelStatus, cancel_status, .requested, .monotonic);
16131923}
16141924
16151925fn swapCancelProtection(userdata: ?*anyopaque, new: Io.CancelProtection) Io.CancelProtection {
......@@ -1622,7 +1932,8 @@ fn swapCancelProtection(userdata: ?*anyopaque, new: Io.CancelProtection) Io.Canc
16221932
16231933fn checkCancel(userdata: ?*anyopaque) Io.Cancelable!void {
16241934 const t: *Threaded = @ptrCast(@alignCast(userdata));
1625 return Thread.getCurrent(t).checkCancel();
1935 _ = t;
1936 return Thread.checkCancel();
16261937}
16271938
16281939fn await(
......@@ -1633,8 +1944,51 @@ fn await(
16331944) void {
16341945 _ = result_alignment;
16351946 const t: *Threaded = @ptrCast(@alignCast(userdata));
1636 const closure: *AsyncClosure = @ptrCast(@alignCast(any_future));
1637 closure.waitAndDeinit(t, result);
1947 const future: *Future = @ptrCast(@alignCast(any_future));
1948 const thread: *Thread = .getCurrent(t);
1949
1950 var num_completed: std.atomic.Value(u32) = .init(0);
1951 future.awaiter = &num_completed;
1952
1953 const pre_await_status = future.status.fetchOr(.{
1954 .tag = .pending_awaited,
1955 .thread = .null,
1956 }, .acq_rel); // acquire results if complete; release `future.awaiter`
1957 switch (pre_await_status.tag) {
1958 .pending => while (thread.futexWait(&num_completed.raw, 0)) {
1959 switch (num_completed.load(.acquire)) { // acquire task results
1960 0 => continue,
1961 1 => break,
1962 else => unreachable, // group was reused before `await` returned
1963 }
1964 } else |err| switch (err) {
1965 error.Canceled => {
1966 const pre_cancel_status = future.status.fetchOr(.{
1967 .tag = .pending_canceled,
1968 .thread = .null,
1969 }, .acq_rel); // acquire results if complete; release `future.awaiter`
1970 switch (pre_cancel_status.tag) {
1971 .pending => unreachable, // invalid state: we already awaited
1972 .pending_awaited => {
1973 const working_thread = pre_cancel_status.thread.unpack();
1974 future.waitForCancelWithSignaling(t, &num_completed, @alignCast(working_thread));
1975 },
1976 .pending_canceled => unreachable, // `await` raced with `cancel`
1977 .done => {
1978 // The task just finished, but we still need to wait for the signal, because the
1979 // task thread already figured out that they need to update `future.awaiter`.
1980 future.waitForCancelWithSignaling(t, &num_completed, null);
1981 },
1982 }
1983 recancel(t);
1984 },
1985 },
1986 .pending_awaited => unreachable, // `await` raced with `await`
1987 .pending_canceled => unreachable, // `await` raced with `cancel`
1988 .done => {},
1989 }
1990 @memcpy(result, future.resultPointer());
1991 future.destroy(t.allocator);
16381992}
16391993
16401994fn cancel(
......@@ -1645,9 +1999,26 @@ fn cancel(
16451999) void {
16462000 _ = result_alignment;
16472001 const t: *Threaded = @ptrCast(@alignCast(userdata));
1648 const ac: *AsyncClosure = @ptrCast(@alignCast(any_future));
1649 ac.closure.requestCancel(t);
1650 ac.waitAndDeinit(t, result);
2002 const future: *Future = @ptrCast(@alignCast(any_future));
2003
2004 var num_completed: std.atomic.Value(u32) = .init(0);
2005 future.awaiter = &num_completed;
2006
2007 const pre_cancel_status = future.status.fetchOr(.{
2008 .tag = .pending_canceled,
2009 .thread = .null,
2010 }, .acq_rel); // acquire results if complete; release `future.awaiter`
2011 switch (pre_cancel_status.tag) {
2012 .pending => {
2013 const working_thread = pre_cancel_status.thread.unpack();
2014 future.waitForCancelWithSignaling(t, &num_completed, @alignCast(working_thread));
2015 },
2016 .pending_awaited => unreachable, // `await` raced with `await`
2017 .pending_canceled => unreachable, // `await` raced with `cancel`
2018 .done => {},
2019 }
2020 @memcpy(result, future.resultPointer());
2021 future.destroy(t.allocator);
16512022}
16522023
16532024fn futexWait(userdata: ?*anyopaque, ptr: *const u32, expected: u32, timeout: Io.Timeout) Io.Cancelable!void {
......@@ -8555,32 +8926,69 @@ fn sleepPosix(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void {
85558926fn select(userdata: ?*anyopaque, futures: []const *Io.AnyFuture) Io.Cancelable!usize {
85568927 const t: *Threaded = @ptrCast(@alignCast(userdata));
85578928
8558 var event: Io.Event = .unset;
8929 var num_completed: std.atomic.Value(u32) = .init(0);
85598930
8560 for (futures, 0..) |future, i| {
8561 const closure: *AsyncClosure = @ptrCast(@alignCast(future));
8562 if (@atomicRmw(?*Io.Event, &closure.select_condition, .Xchg, &event, .seq_cst) == AsyncClosure.done_event) {
8563 for (futures[0..i]) |cleanup_future| {
8564 const cleanup_closure: *AsyncClosure = @ptrCast(@alignCast(cleanup_future));
8565 if (@atomicRmw(?*Io.Event, &cleanup_closure.select_condition, .Xchg, null, .seq_cst) == AsyncClosure.done_event) {
8566 cleanup_closure.event.waitUncancelable(ioBasic(t)); // Ensure no reference to our stack-allocated event.
8567 }
8568 }
8569 return i;
8931 for (futures, 0..) |any_future, i| {
8932 const future: *Future = @ptrCast(@alignCast(any_future));
8933 future.awaiter = &num_completed;
8934 const old_status = future.status.fetchOr(
8935 .{ .tag = .pending_awaited, .thread = .null },
8936 .release, // release `future.awaiter`
8937 );
8938 switch (old_status.tag) {
8939 .pending => {},
8940 .pending_awaited => unreachable, // `await` raced with `select`
8941 .pending_canceled => unreachable, // `cancel` raced with `select`
8942 .done => {
8943 future.status.store(old_status, .monotonic);
8944 _ = finishSelect(&num_completed, futures[0..i]);
8945 return i;
8946 },
85708947 }
85718948 }
85728949
8573 try event.wait(ioBasic(t));
8950 errdefer _ = finishSelect(&num_completed, futures);
8951 const thread: *Thread = .getCurrent(t);
85748952
8575 var result: ?usize = null;
8576 for (futures, 0..) |future, i| {
8577 const closure: *AsyncClosure = @ptrCast(@alignCast(future));
8578 if (@atomicRmw(?*Io.Event, &closure.select_condition, .Xchg, null, .seq_cst) == AsyncClosure.done_event) {
8579 closure.event.waitUncancelable(ioBasic(t)); // Ensure no reference to our stack-allocated event.
8580 if (result == null) result = i; // In case multiple are ready, return first.
8581 }
8953 while (true) {
8954 const n = num_completed.load(.acquire);
8955 if (n > 0) break;
8956 assert(n < futures.len);
8957 try thread.futexWait(&num_completed.raw, n);
8958 }
8959 return finishSelect(&num_completed, futures).?;
8960}
8961fn finishSelect(
8962 num_completed: *std.atomic.Value(u32),
8963 futures: []const *Io.AnyFuture,
8964) ?usize {
8965 var completed_index: ?usize = null;
8966 var expect_completed: u32 = 0;
8967 for (futures, 0..) |any_future, i| {
8968 const future: *Future = @ptrCast(@alignCast(any_future));
8969 // This operation will convert `.pending_awaited` to `.pending`, or leave `.done` untouched.
8970 switch (future.status.fetchAnd(
8971 .{ .tag = @enumFromInt(0b10), .thread = .all_ones },
8972 .monotonic,
8973 ).tag) {
8974 .pending_awaited => {},
8975 .pending => unreachable,
8976 .pending_canceled => unreachable,
8977 .done => {
8978 expect_completed += 1;
8979 completed_index = i;
8980 },
8981 }
8982 }
8983 // If any future has just finished, wait for it to signal `num_completed` to avoid dangling
8984 // references to stack memory.
8985 while (true) {
8986 const n = num_completed.load(.acquire);
8987 if (n == expect_completed) break;
8988 assert(n < expect_completed);
8989 Thread.futexWaitUncancelable(&num_completed.raw, n);
85828990 }
8583 return result.?;
8991 return completed_index;
85848992}
85858993
85868994fn netListenIpPosix(