authorgravatar for jacobly@ziglang.orgJacob Young <jacobly@ziglang.org> 2025-04-01 03:45:31-04:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-10-02 16:30:59-07:00
logb174777437ffc537f58e45cd54cc4f9bcf5305d9
tree7ab99a89b399badce3e7a12a276caab4397200af
parent5c4ddb8d3593fd2d0aa2b65c66d6710ed8f6113b

EventLoop: implement detached fibers


1 files changed, 78 insertions(+), 46 deletions(-)

lib/std/Io/EventLoop.zig+78-46
...@@ -9,9 +9,12 @@ const IoUring = std.os.linux.IoUring;...@@ -9,9 +9,12 @@ const IoUring = std.os.linux.IoUring;
99
10/// Must be a thread-safe allocator.10/// Must be a thread-safe allocator.
11gpa: Allocator,11gpa: Allocator,
12mutex: std.Thread.Mutex,
13main_fiber_buffer: [@sizeOf(Fiber) + Fiber.max_result_size]u8 align(@alignOf(Fiber)),12main_fiber_buffer: [@sizeOf(Fiber) + Fiber.max_result_size]u8 align(@alignOf(Fiber)),
14threads: Thread.List,13threads: Thread.List,
14detached: struct {
15 mutex: std.Io.Mutex,
16 list: std.DoublyLinkedList(void),
17},
1518
16/// Empirically saw >128KB being used by the self-hosted backend to panic.19/// Empirically saw >128KB being used by the self-hosted backend to panic.
17const idle_stack_size = 256 * 1024;20const idle_stack_size = 256 * 1024;
...@@ -167,13 +170,16 @@ pub fn init(el: *EventLoop, gpa: Allocator) !void {...@@ -167,13 +170,16 @@ pub fn init(el: *EventLoop, gpa: Allocator) !void {
167 errdefer gpa.free(allocated_slice);170 errdefer gpa.free(allocated_slice);
168 el.* = .{171 el.* = .{
169 .gpa = gpa,172 .gpa = gpa,
170 .mutex = .{},
171 .main_fiber_buffer = undefined,173 .main_fiber_buffer = undefined,
172 .threads = .{174 .threads = .{
173 .allocated = @ptrCast(allocated_slice[0..threads_size]),175 .allocated = @ptrCast(allocated_slice[0..threads_size]),
174 .reserved = 1,176 .reserved = 1,
175 .active = 1,177 .active = 1,
176 },178 },
179 .detached = .{
180 .mutex = .init,
181 .list = .{},
182 },
177 };183 };
178 const main_fiber: *Fiber = @ptrCast(&el.main_fiber_buffer);184 const main_fiber: *Fiber = @ptrCast(&el.main_fiber_buffer);
179 main_fiber.* = .{185 main_fiber.* = .{
...@@ -207,6 +213,23 @@ pub fn init(el: *EventLoop, gpa: Allocator) !void {...@@ -207,6 +213,23 @@ pub fn init(el: *EventLoop, gpa: Allocator) !void {
207}213}
208214
209pub fn deinit(el: *EventLoop) void {215pub fn deinit(el: *EventLoop) void {
216 while (true) cancel(el, detached_future: {
217 el.detached.mutex.lock(el.io()) catch |err| switch (err) {
218 error.Canceled => unreachable, // main fiber cannot be canceled
219 };
220 defer el.detached.mutex.unlock(el.io());
221 const detached: *DetachedClosure = @fieldParentPtr(
222 "detached_queue_node",
223 el.detached.list.pop() orelse break,
224 );
225 // notify the detached fiber that it is no longer allowed to recycle itself
226 detached.detached_queue_node = .{
227 .prev = &detached.detached_queue_node,
228 .next = &detached.detached_queue_node,
229 .data = {},
230 };
231 break :detached_future @ptrCast(detached.fiber);
232 }, &.{}, .@"1");
210 const active_threads = @atomicLoad(u32, &el.threads.active, .acquire);233 const active_threads = @atomicLoad(u32, &el.threads.active, .acquire);
211 for (el.threads.allocated[0..active_threads]) |*thread| {234 for (el.threads.allocated[0..active_threads]) |*thread| {
212 const ready_fiber = @atomicLoad(?*Fiber, &thread.ready_queue, .monotonic);235 const ready_fiber = @atomicLoad(?*Fiber, &thread.ready_queue, .monotonic);
...@@ -460,7 +483,7 @@ const SwitchMessage = struct {...@@ -460,7 +483,7 @@ const SwitchMessage = struct {
460 const PendingTask = union(enum) {483 const PendingTask = union(enum) {
461 nothing,484 nothing,
462 reschedule,485 reschedule,
463 recycle: *Fiber,486 recycle,
464 register_awaiter: *?*Fiber,487 register_awaiter: *?*Fiber,
465 mutex_lock: struct {488 mutex_lock: struct {
466 prev_state: Io.Mutex.State,489 prev_state: Io.Mutex.State,
...@@ -483,8 +506,10 @@ const SwitchMessage = struct {...@@ -483,8 +506,10 @@ const SwitchMessage = struct {
483 assert(prev_fiber.queue_next == null);506 assert(prev_fiber.queue_next == null);
484 el.schedule(thread, .{ .head = prev_fiber, .tail = prev_fiber });507 el.schedule(thread, .{ .head = prev_fiber, .tail = prev_fiber });
485 },508 },
486 .recycle => |fiber| {509 .recycle => {
487 el.recycle(fiber);510 const prev_fiber: *Fiber = @alignCast(@fieldParentPtr("context", message.contexts.prev));
511 assert(prev_fiber.queue_next == null);
512 el.recycle(prev_fiber);
488 },513 },
489 .register_awaiter => |awaiter| {514 .register_awaiter => |awaiter| {
490 const prev_fiber: *Fiber = @alignCast(@fieldParentPtr("context", message.contexts.prev));515 const prev_fiber: *Fiber = @alignCast(@fieldParentPtr("context", message.contexts.prev));
...@@ -609,21 +634,7 @@ fn fiberEntry() callconv(.naked) void {...@@ -609,21 +634,7 @@ fn fiberEntry() callconv(.naked) void {
609 switch (builtin.cpu.arch) {634 switch (builtin.cpu.arch) {
610 .x86_64 => asm volatile (635 .x86_64 => asm volatile (
611 \\ leaq 8(%%rsp), %%rdi636 \\ leaq 8(%%rsp), %%rdi
612 \\ jmp %[AsyncClosure_call:P]637 \\ jmpq *(%%rsp)
613 :
614 : [AsyncClosure_call] "X" (&AsyncClosure.call),
615 ),
616 else => |arch| @compileError("unimplemented architecture: " ++ @tagName(arch)),
617 }
618}
619
620fn fiberEntryDetached() callconv(.naked) void {
621 switch (builtin.cpu.arch) {
622 .x86_64 => asm volatile (
623 \\ leaq 8(%%rsp), %%rdi
624 \\ jmp %[DetachedClosure_call:P]
625 :
626 : [DetachedClosure_call] "X" (&DetachedClosure.call),
627 ),638 ),
628 else => |arch| @compileError("unimplemented architecture: " ++ @tagName(arch)),639 else => |arch| @compileError("unimplemented architecture: " ++ @tagName(arch)),
629 }640 }
...@@ -649,29 +660,6 @@ const AsyncClosure = struct {...@@ -649,29 +660,6 @@ const AsyncClosure = struct {
649 }660 }
650};661};
651662
652const DetachedClosure = struct {
653 event_loop: *EventLoop,
654 fiber: *Fiber,
655 start: *const fn (context: *const anyopaque) void,
656
657 fn contextPointer(closure: *DetachedClosure) [*]align(Fiber.max_context_align.toByteUnits()) u8 {
658 return @alignCast(@as([*]u8, @ptrCast(closure)) + @sizeOf(DetachedClosure));
659 }
660
661 fn call(closure: *DetachedClosure, message: *const SwitchMessage) callconv(.withStackAlign(.c, @alignOf(DetachedClosure))) noreturn {
662 message.handle(closure.event_loop);
663 std.log.debug("{*} performing async detached", .{closure.fiber});
664 closure.start(closure.contextPointer());
665 const awaiter = @atomicRmw(?*Fiber, &closure.fiber.awaiter, .Xchg, Fiber.finished, .acq_rel);
666 if (awaiter) |a| {
667 closure.event_loop.yield(a, .nothing);
668 } else {
669 closure.event_loop.yield(null, .{ .recycle = closure.fiber });
670 }
671 unreachable; // switched to dead fiber
672 }
673};
674
675fn @"async"(663fn @"async"(
676 userdata: ?*anyopaque,664 userdata: ?*anyopaque,
677 result: []u8,665 result: []u8,
...@@ -695,11 +683,13 @@ fn @"async"(...@@ -695,11 +683,13 @@ fn @"async"(
695 const closure: *AsyncClosure = @ptrFromInt(Fiber.max_context_align.max(.of(AsyncClosure)).backward(683 const closure: *AsyncClosure = @ptrFromInt(Fiber.max_context_align.max(.of(AsyncClosure)).backward(
696 @intFromPtr(fiber.allocatedEnd()) - Fiber.max_context_size,684 @intFromPtr(fiber.allocatedEnd()) - Fiber.max_context_size,
697 ) - @sizeOf(AsyncClosure));685 ) - @sizeOf(AsyncClosure));
686 const stack_end: [*]usize = @alignCast(@ptrCast(closure));
687 (stack_end - 1)[0..1].* = .{@intFromPtr(&AsyncClosure.call)};
698 fiber.* = .{688 fiber.* = .{
699 .required_align = {},689 .required_align = {},
700 .context = switch (builtin.cpu.arch) {690 .context = switch (builtin.cpu.arch) {
701 .x86_64 => .{691 .x86_64 => .{
702 .rsp = @intFromPtr(closure) - @sizeOf(usize),692 .rsp = @intFromPtr(stack_end - 1),
703 .rbp = 0,693 .rbp = 0,
704 .rip = @intFromPtr(&fiberEntry),694 .rip = @intFromPtr(&fiberEntry),
705 },695 },
...@@ -722,6 +712,34 @@ fn @"async"(...@@ -722,6 +712,34 @@ fn @"async"(
722 return @ptrCast(fiber);712 return @ptrCast(fiber);
723}713}
724714
715const DetachedClosure = struct {
716 event_loop: *EventLoop,
717 fiber: *Fiber,
718 start: *const fn (context: *const anyopaque) void,
719 detached_queue_node: std.DoublyLinkedList(void).Node,
720
721 fn contextPointer(closure: *DetachedClosure) [*]align(Fiber.max_context_align.toByteUnits()) u8 {
722 return @alignCast(@as([*]u8, @ptrCast(closure)) + @sizeOf(DetachedClosure));
723 }
724
725 fn call(closure: *DetachedClosure, message: *const SwitchMessage) callconv(.withStackAlign(.c, @alignOf(DetachedClosure))) noreturn {
726 message.handle(closure.event_loop);
727 std.log.debug("{*} performing async detached", .{closure.fiber});
728 closure.start(closure.contextPointer());
729 const awaiter = @atomicRmw(?*Fiber, &closure.fiber.awaiter, .Xchg, Fiber.finished, .acq_rel);
730 closure.event_loop.yield(awaiter, pending_task: {
731 closure.event_loop.detached.mutex.lock(closure.event_loop.io()) catch |err| switch (err) {
732 error.Canceled => break :pending_task .nothing,
733 };
734 defer closure.event_loop.detached.mutex.unlock(closure.event_loop.io());
735 if (closure.detached_queue_node.next == &closure.detached_queue_node) break :pending_task .nothing;
736 closure.event_loop.detached.list.remove(&closure.detached_queue_node);
737 break :pending_task .recycle;
738 });
739 unreachable; // switched to dead fiber
740 }
741};
742
725fn go(743fn go(
726 userdata: ?*anyopaque,744 userdata: ?*anyopaque,
727 context: []const u8,745 context: []const u8,
...@@ -742,13 +760,15 @@ fn go(...@@ -742,13 +760,15 @@ fn go(
742 const closure: *DetachedClosure = @ptrFromInt(Fiber.max_context_align.max(.of(DetachedClosure)).backward(760 const closure: *DetachedClosure = @ptrFromInt(Fiber.max_context_align.max(.of(DetachedClosure)).backward(
743 @intFromPtr(fiber.allocatedEnd()) - Fiber.max_context_size,761 @intFromPtr(fiber.allocatedEnd()) - Fiber.max_context_size,
744 ) - @sizeOf(DetachedClosure));762 ) - @sizeOf(DetachedClosure));
763 const stack_end: [*]usize = @alignCast(@ptrCast(closure));
764 (stack_end - 1)[0..1].* = .{@intFromPtr(&DetachedClosure.call)};
745 fiber.* = .{765 fiber.* = .{
746 .required_align = {},766 .required_align = {},
747 .context = switch (builtin.cpu.arch) {767 .context = switch (builtin.cpu.arch) {
748 .x86_64 => .{768 .x86_64 => .{
749 .rsp = @intFromPtr(closure) - @sizeOf(usize),769 .rsp = @intFromPtr(stack_end - 1),
750 .rbp = 0,770 .rbp = 0,
751 .rip = @intFromPtr(&fiberEntryDetached),771 .rip = @intFromPtr(&fiberEntry),
752 },772 },
753 else => |arch| @compileError("unimplemented architecture: " ++ @tagName(arch)),773 else => |arch| @compileError("unimplemented architecture: " ++ @tagName(arch)),
754 },774 },
...@@ -761,7 +781,19 @@ fn go(...@@ -761,7 +781,19 @@ fn go(
761 .event_loop = event_loop,781 .event_loop = event_loop,
762 .fiber = fiber,782 .fiber = fiber,
763 .start = start,783 .start = start,
784 .detached_queue_node = .{ .data = {} },
764 };785 };
786 {
787 event_loop.detached.mutex.lock(event_loop.io()) catch |err| switch (err) {
788 error.Canceled => {
789 event_loop.recycle(fiber);
790 start(context.ptr);
791 return;
792 },
793 };
794 defer event_loop.detached.mutex.unlock(event_loop.io());
795 event_loop.detached.list.append(&closure.detached_queue_node);
796 }
765 @memcpy(closure.contextPointer(), context);797 @memcpy(closure.contextPointer(), context);
766798
767 event_loop.schedule(current_thread, .{ .head = fiber, .tail = fiber });799 event_loop.schedule(current_thread, .{ .head = fiber, .tail = fiber });