EventLoop: add threads

This commit is contained in:
Jacob Young 2025-03-27 17:19:53 -04:00 committed by Andrew Kelley
parent 03bb08d337
commit c7b406f2ad

View File

@ -7,15 +7,25 @@ const EventLoop = @This();
gpa: Allocator, gpa: Allocator,
mutex: std.Thread.Mutex, mutex: std.Thread.Mutex,
cond: std.Thread.Condition,
queue: std.DoublyLinkedList(void), queue: std.DoublyLinkedList(void),
free: std.DoublyLinkedList(void), free: std.DoublyLinkedList(void),
main_fiber_buffer: [@sizeOf(Fiber) + max_result_len]u8 align(@alignOf(Fiber)), main_fiber_buffer: [@sizeOf(Fiber) + max_result_len]u8 align(@alignOf(Fiber)),
exiting: bool,
idle_count: usize,
threads: std.ArrayListUnmanaged(Thread),
threadlocal var current_thread: *Thread = undefined;
threadlocal var current_fiber: *Fiber = undefined; threadlocal var current_fiber: *Fiber = undefined;
const max_result_len = 64; const max_result_len = 64;
const min_stack_size = 4 * 1024 * 1024; const min_stack_size = 4 * 1024 * 1024;
const Thread = struct {
thread: std.Thread,
idle_fiber: Fiber,
};
const Fiber = struct { const Fiber = struct {
context: Context, context: Context,
awaiter: ?*Fiber, awaiter: ?*Fiber,
@ -23,32 +33,58 @@ const Fiber = struct {
const finished: ?*Fiber = @ptrFromInt(std.mem.alignBackward(usize, std.math.maxInt(usize), @alignOf(Fiber))); const finished: ?*Fiber = @ptrFromInt(std.mem.alignBackward(usize, std.math.maxInt(usize), @alignOf(Fiber)));
fn resultPointer(f: *Fiber) [*]u8 { fn allocatedSlice(f: *Fiber) []align(@alignOf(Fiber)) u8 {
const base: [*]u8 = @ptrCast(f); const base: [*]align(@alignOf(Fiber)) u8 = @ptrCast(f);
return base + @sizeOf(Fiber); return base[0..std.mem.alignForward(
}
fn stackEndPointer(f: *Fiber) [*]u8 {
const base: [*]u8 = @ptrCast(f);
return base + std.mem.alignForward(
usize, usize,
@sizeOf(Fiber) + max_result_len + min_stack_size, @sizeOf(Fiber) + max_result_len + min_stack_size,
std.heap.page_size_max, std.heap.page_size_max,
); )];
}
fn resultSlice(f: *Fiber) []u8 {
const base: [*]align(@alignOf(Fiber)) u8 = @ptrCast(f);
return base[@sizeOf(Fiber)..][0..max_result_len];
}
fn stackEndPointer(f: *Fiber) [*]u8 {
const allocated_slice = f.allocatedSlice();
return allocated_slice[allocated_slice.len..].ptr;
} }
}; };
pub fn init(el: *EventLoop, gpa: Allocator) void { pub fn init(el: *EventLoop, gpa: Allocator) error{OutOfMemory}!void {
el.* = .{ el.* = .{
.gpa = gpa, .gpa = gpa,
.mutex = .{}, .mutex = .{},
.cond = .{},
.queue = .{}, .queue = .{},
.free = .{}, .free = .{},
.main_fiber_buffer = undefined, .main_fiber_buffer = undefined,
.exiting = false,
.idle_count = 0,
.threads = try .initCapacity(gpa, @max(std.Thread.getCpuCount() catch 1, 1)),
}; };
current_thread = el.threads.addOneAssumeCapacity();
current_fiber = @ptrCast(&el.main_fiber_buffer); current_fiber = @ptrCast(&el.main_fiber_buffer);
} }
pub fn deinit(el: *EventLoop) void {
{
el.mutex.lock();
defer el.mutex.unlock();
assert(el.queue.len == 0); // pending async
el.exiting = true;
}
el.cond.broadcast();
while (el.free.pop()) |free_node| {
const free_fiber: *Fiber = @fieldParentPtr("queue_node", free_node);
el.gpa.free(free_fiber.allocatedSlice());
}
for (el.threads.items[1..]) |*thread| thread.thread.join();
el.threads.deinit(el.gpa);
}
fn allocateFiber(el: *EventLoop, result_len: usize) error{OutOfMemory}!*Fiber { fn allocateFiber(el: *EventLoop, result_len: usize) error{OutOfMemory}!*Fiber {
assert(result_len <= max_result_len); assert(result_len <= max_result_len);
const free_node = free_node: { const free_node = free_node: {
@ -73,10 +109,8 @@ fn yield(el: *EventLoop, optional_fiber: ?*Fiber, register_awaiter: ?*?*Fiber) v
break :ready_node el.queue.pop(); break :ready_node el.queue.pop();
}) |ready_node| }) |ready_node|
@fieldParentPtr("queue_node", ready_node) @fieldParentPtr("queue_node", ready_node)
else if (register_awaiter) |_| // time to switch to an idle fiber? else
@panic("no other fiber to switch to in order to be able to register this fiber as an awaiter") &current_thread.idle_fiber;
else // nothing to do
return;
const message: SwitchMessage = .{ const message: SwitchMessage = .{
.prev_context = &current_fiber.context, .prev_context = &current_fiber.context,
.ready_context = &ready_fiber.context, .ready_context = &ready_fiber.context,
@ -90,20 +124,44 @@ fn yield(el: *EventLoop, optional_fiber: ?*Fiber, register_awaiter: ?*?*Fiber) v
} }
fn schedule(el: *EventLoop, fiber: *Fiber) void { fn schedule(el: *EventLoop, fiber: *Fiber) void {
signal: {
el.mutex.lock(); el.mutex.lock();
defer el.mutex.unlock(); defer el.mutex.unlock();
el.queue.append(&fiber.queue_node); el.queue.append(&fiber.queue_node);
if (el.idle_count > 0) break :signal;
if (el.threads.items.len == el.threads.capacity) return;
const thread = el.threads.addOneAssumeCapacity();
thread.thread = std.Thread.spawn(.{
.stack_size = min_stack_size,
.allocator = el.gpa,
}, threadEntry, .{ el, thread }) catch return;
}
el.cond.signal();
} }
fn recycle(el: *EventLoop, fiber: *Fiber) void { fn recycle(el: *EventLoop, fiber: *Fiber) void {
std.log.debug("recyling {*}", .{fiber}); std.log.debug("recyling {*}", .{fiber});
fiber.awaiter = undefined; fiber.awaiter = undefined;
@memset(fiber.resultPointer()[0..max_result_len], undefined); @memset(fiber.resultSlice(), undefined);
el.mutex.lock(); el.mutex.lock();
defer el.mutex.unlock(); defer el.mutex.unlock();
el.free.append(&fiber.queue_node); el.free.append(&fiber.queue_node);
} }
fn threadEntry(el: *EventLoop, thread: *Thread) void {
current_thread = thread;
current_fiber = &thread.idle_fiber;
while (true) {
el.yield(null, null);
el.mutex.lock();
defer el.mutex.unlock();
if (el.exiting) return;
el.idle_count += 1;
defer el.idle_count -= 1;
el.cond.wait(&el.mutex);
}
}
const SwitchMessage = extern struct { const SwitchMessage = extern struct {
prev_context: *Context, prev_context: *Context,
ready_context: *Context, ready_context: *Context,
@ -209,7 +267,7 @@ const AsyncClosure = struct {
fn call(closure: *AsyncClosure, message: *const SwitchMessage) callconv(.c) noreturn { fn call(closure: *AsyncClosure, message: *const SwitchMessage) callconv(.c) noreturn {
message.handle(closure.event_loop); message.handle(closure.event_loop);
std.log.debug("{*} performing async", .{closure.fiber}); std.log.debug("{*} performing async", .{closure.fiber});
closure.start(closure.context, closure.fiber.resultPointer()); closure.start(closure.context, closure.fiber.resultSlice().ptr);
const awaiter = @atomicRmw(?*Fiber, &closure.fiber.awaiter, .Xchg, Fiber.finished, .acq_rel); const awaiter = @atomicRmw(?*Fiber, &closure.fiber.awaiter, .Xchg, Fiber.finished, .acq_rel);
closure.event_loop.yield(awaiter, null); closure.event_loop.yield(awaiter, null);
unreachable; // switched to dead fiber unreachable; // switched to dead fiber
@ -219,7 +277,7 @@ const AsyncClosure = struct {
pub fn @"await"(userdata: ?*anyopaque, any_future: *std.Io.AnyFuture, result: []u8) void { pub fn @"await"(userdata: ?*anyopaque, any_future: *std.Io.AnyFuture, result: []u8) void {
const event_loop: *EventLoop = @alignCast(@ptrCast(userdata)); const event_loop: *EventLoop = @alignCast(@ptrCast(userdata));
const future_fiber: *Fiber = @alignCast(@ptrCast(any_future)); const future_fiber: *Fiber = @alignCast(@ptrCast(any_future));
const result_src = future_fiber.resultPointer()[0..result.len]; const result_src = future_fiber.resultSlice()[0..result.len];
if (@atomicLoad(?*Fiber, &future_fiber.awaiter, .acquire) != Fiber.finished) event_loop.yield(null, &future_fiber.awaiter); if (@atomicLoad(?*Fiber, &future_fiber.awaiter, .acquire) != Fiber.finished) event_loop.yield(null, &future_fiber.awaiter);
@memcpy(result, result_src); @memcpy(result, result_src);
event_loop.recycle(future_fiber); event_loop.recycle(future_fiber);