| ... | @@ -11,53 +11,69 @@ pub const TcpServer = struct { | ... | @@ -11,53 +11,69 @@ pub const TcpServer = struct { |
| 11 | handleRequestFn: async<*mem.Allocator> fn (*TcpServer, *const std.net.Address, *const std.os.File) void, | 11 | handleRequestFn: async<*mem.Allocator> fn (*TcpServer, *const std.net.Address, *const std.os.File) void, |
| 12 | | 12 | |
| 13 | loop: *Loop, | 13 | loop: *Loop, |
| 14 | sockfd: i32, | 14 | sockfd: ?i32, |
| 15 | accept_coro: ?promise, | 15 | accept_coro: ?promise, |
| 16 | listen_address: std.net.Address, | 16 | listen_address: std.net.Address, |
| 17 | | 17 | |
| 18 | waiting_for_emfile_node: PromiseNode, | 18 | waiting_for_emfile_node: PromiseNode, |
| | 19 | listen_resume_node: event.Loop.ResumeNode, |
| 19 | | 20 | |
| 20 | const PromiseNode = std.LinkedList(promise).Node; | 21 | const PromiseNode = std.LinkedList(promise).Node; |
| 21 | | 22 | |
| 22 | pub fn init(loop: *Loop) !TcpServer { | 23 | pub fn init(loop: *Loop) TcpServer { |
| 23 | const sockfd = try std.os.posixSocket(posix.AF_INET, posix.SOCK_STREAM | posix.SOCK_CLOEXEC | posix.SOCK_NONBLOCK, posix.PROTO_tcp); | | |
| 24 | errdefer std.os.close(sockfd); | | |
| 25 | | | |
| 26 | // TODO can't initialize handler coroutine here because we need well defined copy elision | 24 | // TODO can't initialize handler coroutine here because we need well defined copy elision |
| 27 | return TcpServer{ | 25 | return TcpServer{ |
| 28 | .loop = loop, | 26 | .loop = loop, |
| 29 | .sockfd = sockfd, | 27 | .sockfd = null, |
| 30 | .accept_coro = null, | 28 | .accept_coro = null, |
| 31 | .handleRequestFn = undefined, | 29 | .handleRequestFn = undefined, |
| 32 | .waiting_for_emfile_node = undefined, | 30 | .waiting_for_emfile_node = undefined, |
| 33 | .listen_address = undefined, | 31 | .listen_address = undefined, |
| | 32 | .listen_resume_node = event.Loop.ResumeNode{ |
| | 33 | .id = event.Loop.ResumeNode.Id.Basic, |
| | 34 | .handle = undefined, |
| | 35 | }, |
| 34 | }; | 36 | }; |
| 35 | } | 37 | } |
| 36 | | 38 | |
| 37 | pub fn listen(self: *TcpServer, address: *const std.net.Address, handleRequestFn: async<*mem.Allocator> fn (*TcpServer, *const std.net.Address, *const std.os.File) void) !void { | 39 | pub fn listen( |
| | 40 | self: *TcpServer, |
| | 41 | address: *const std.net.Address, |
| | 42 | handleRequestFn: async<*mem.Allocator> fn (*TcpServer, *const std.net.Address, *const std.os.File) void, |
| | 43 | ) !void { |
| 38 | self.handleRequestFn = handleRequestFn; | 44 | self.handleRequestFn = handleRequestFn; |
| 39 | | 45 | |
| 40 | try std.os.posixBind(self.sockfd, &address.os_addr); | 46 | const sockfd = try std.os.posixSocket(posix.AF_INET, posix.SOCK_STREAM | posix.SOCK_CLOEXEC | posix.SOCK_NONBLOCK, posix.PROTO_tcp); |
| 41 | try std.os.posixListen(self.sockfd, posix.SOMAXCONN); | 47 | errdefer std.os.close(sockfd); |
| 42 | self.listen_address = std.net.Address.initPosix(try std.os.posixGetSockName(self.sockfd)); | 48 | self.sockfd = sockfd; |
| | 49 | |
| | 50 | try std.os.posixBind(sockfd, &address.os_addr); |
| | 51 | try std.os.posixListen(sockfd, posix.SOMAXCONN); |
| | 52 | self.listen_address = std.net.Address.initPosix(try std.os.posixGetSockName(sockfd)); |
| 43 | | 53 | |
| 44 | self.accept_coro = try async<self.loop.allocator> TcpServer.handler(self); | 54 | self.accept_coro = try async<self.loop.allocator> TcpServer.handler(self); |
| 45 | errdefer cancel self.accept_coro.?; | 55 | errdefer cancel self.accept_coro.?; |
| 46 | | 56 | |
| 47 | try self.loop.addFd(self.sockfd, self.accept_coro.?); | 57 | self.listen_resume_node.handle = self.accept_coro.?; |
| 48 | errdefer self.loop.removeFd(self.sockfd); | 58 | try self.loop.addFd(sockfd, &self.listen_resume_node); |
| | 59 | errdefer self.loop.removeFd(sockfd); |
| | 60 | } |
| | 61 | |
| | 62 | /// Stop listening |
| | 63 | pub fn close(self: *TcpServer) void { |
| | 64 | self.loop.removeFd(self.sockfd.?); |
| | 65 | std.os.close(self.sockfd.?); |
| 49 | } | 66 | } |
| 50 | | 67 | |
| 51 | pub fn deinit(self: *TcpServer) void { | 68 | pub fn deinit(self: *TcpServer) void { |
| 52 | self.loop.removeFd(self.sockfd); | | |
| 53 | if (self.accept_coro) |accept_coro| cancel accept_coro; | 69 | if (self.accept_coro) |accept_coro| cancel accept_coro; |
| 54 | std.os.close(self.sockfd); | 70 | if (self.sockfd) |sockfd| std.os.close(sockfd); |
| 55 | } | 71 | } |
| 56 | | 72 | |
| 57 | pub async fn handler(self: *TcpServer) void { | 73 | pub async fn handler(self: *TcpServer) void { |
| 58 | while (true) { | 74 | while (true) { |
| 59 | var accepted_addr: std.net.Address = undefined; | 75 | var accepted_addr: std.net.Address = undefined; |
| 60 | if (std.os.posixAccept(self.sockfd, &accepted_addr.os_addr, posix.SOCK_NONBLOCK | posix.SOCK_CLOEXEC)) |accepted_fd| { | 76 | if (std.os.posixAccept(self.sockfd.?, &accepted_addr.os_addr, posix.SOCK_NONBLOCK | posix.SOCK_CLOEXEC)) |accepted_fd| { |
| 61 | var socket = std.os.File.openHandle(accepted_fd); | 77 | var socket = std.os.File.openHandle(accepted_fd); |
| 62 | _ = async<self.loop.allocator> self.handleRequestFn(self, accepted_addr, socket) catch |err| switch (err) { | 78 | _ = async<self.loop.allocator> self.handleRequestFn(self, accepted_addr, socket) catch |err| switch (err) { |
| 63 | error.OutOfMemory => { | 79 | error.OutOfMemory => { |
| ... | @@ -95,32 +111,65 @@ pub const TcpServer = struct { | ... | @@ -95,32 +111,65 @@ pub const TcpServer = struct { |
| 95 | | 111 | |
| 96 | pub const Loop = struct { | 112 | pub const Loop = struct { |
| 97 | allocator: *mem.Allocator, | 113 | allocator: *mem.Allocator, |
| 98 | keep_running: bool, | | |
| 99 | next_tick_queue: std.atomic.QueueMpsc(promise), | 114 | next_tick_queue: std.atomic.QueueMpsc(promise), |
| 100 | os_data: OsData, | 115 | os_data: OsData, |
| | 116 | dispatch_lock: u8, // TODO make this a bool |
| | 117 | pending_event_count: usize, |
| | 118 | extra_threads: []*std.os.Thread, |
| | 119 | final_resume_node: ResumeNode, |
| 101 | | 120 | |
| 102 | const OsData = switch (builtin.os) { | 121 | pub const NextTickNode = std.atomic.QueueMpsc(promise).Node; |
| 103 | builtin.Os.linux => struct { | 122 | |
| 104 | epollfd: i32, | 123 | pub const ResumeNode = struct { |
| 105 | }, | 124 | id: Id, |
| 106 | else => struct {}, | 125 | handle: promise, |
| | 126 | |
| | 127 | pub const Id = enum { |
| | 128 | Basic, |
| | 129 | Stop, |
| | 130 | EventFd, |
| | 131 | }; |
| | 132 | |
| | 133 | pub const EventFd = struct { |
| | 134 | base: ResumeNode, |
| | 135 | eventfd: i32, |
| | 136 | }; |
| 107 | }; | 137 | }; |
| 108 | | 138 | |
| 109 | pub const NextTickNode = std.atomic.QueueMpsc(promise).Node; | 139 | /// After initialization, call run(). |
| | 140 | /// TODO copy elision / named return values so that the threads referencing *Loop |
| | 141 | /// have the correct pointer value. |
| | 142 | fn initSingleThreaded(self: *Loop, allocator: *mem.Allocator) !void { |
| | 143 | return self.initInternal(allocator, 1); |
| | 144 | } |
| 110 | | 145 | |
| 111 | /// The allocator must be thread-safe because we use it for multiplexing | 146 | /// The allocator must be thread-safe because we use it for multiplexing |
| 112 | /// coroutines onto kernel threads. | 147 | /// coroutines onto kernel threads. |
| 113 | pub fn init(allocator: *mem.Allocator) !Loop { | 148 | /// After initialization, call run(). |
| 114 | var self = Loop{ | 149 | /// TODO copy elision / named return values so that the threads referencing *Loop |
| 115 | .keep_running = true, | 150 | /// have the correct pointer value. |
| | 151 | fn initMultiThreaded(self: *Loop, allocator: *mem.Allocator) !void { |
| | 152 | // TODO check the actual cpu core count |
| | 153 | return self.initInternal(allocator, 4); |
| | 154 | } |
| | 155 | |
| | 156 | /// Thread count is the total thread count. The thread pool size will be |
| | 157 | /// max(thread_count - 1, 0) |
| | 158 | fn initInternal(self: *Loop, allocator: *mem.Allocator, thread_count: usize) !void { |
| | 159 | self.* = Loop{ |
| | 160 | .pending_event_count = 0, |
| 116 | .allocator = allocator, | 161 | .allocator = allocator, |
| 117 | .os_data = undefined, | 162 | .os_data = undefined, |
| 118 | .next_tick_queue = std.atomic.QueueMpsc(promise).init(), | 163 | .next_tick_queue = std.atomic.QueueMpsc(promise).init(), |
| | 164 | .dispatch_lock = 1, // start locked so threads go directly into epoll wait |
| | 165 | .extra_threads = undefined, |
| | 166 | .final_resume_node = ResumeNode{ |
| | 167 | .id = ResumeNode.Id.Stop, |
| | 168 | .handle = undefined, |
| | 169 | }, |
| 119 | }; | 170 | }; |
| 120 | try self.initOsData(); | 171 | try self.initOsData(thread_count); |
| 121 | errdefer self.deinitOsData(); | 172 | errdefer self.deinitOsData(); |
| 122 | | | |
| 123 | return self; | | |
| 124 | } | 173 | } |
| 125 | | 174 | |
| 126 | /// must call stop before deinit | 175 | /// must call stop before deinit |
| ... | @@ -128,13 +177,70 @@ pub const Loop = struct { | ... | @@ -128,13 +177,70 @@ pub const Loop = struct { |
| 128 | self.deinitOsData(); | 177 | self.deinitOsData(); |
| 129 | } | 178 | } |
| 130 | | 179 | |
| 131 | const InitOsDataError = std.os.LinuxEpollCreateError; | 180 | const InitOsDataError = std.os.LinuxEpollCreateError || mem.Allocator.Error || std.os.LinuxEventFdError || |
| | 181 | std.os.SpawnThreadError || std.os.LinuxEpollCtlError; |
| | 182 | |
| | 183 | const wakeup_bytes = []u8{0x1} ** 8; |
| 132 | | 184 | |
| 133 | fn initOsData(self: *Loop) InitOsDataError!void { | 185 | fn initOsData(self: *Loop, thread_count: usize) InitOsDataError!void { |
| 134 | switch (builtin.os) { | 186 | switch (builtin.os) { |
| 135 | builtin.Os.linux => { | 187 | builtin.Os.linux => { |
| 136 | self.os_data.epollfd = try std.os.linuxEpollCreate(std.os.linux.EPOLL_CLOEXEC); | 188 | const extra_thread_count = thread_count - 1; |
| | 189 | self.os_data.available_eventfd_resume_nodes = std.atomic.Stack(ResumeNode.EventFd).init(); |
| | 190 | self.os_data.eventfd_resume_nodes = try self.allocator.alloc( |
| | 191 | std.atomic.Stack(ResumeNode.EventFd).Node, |
| | 192 | extra_thread_count, |
| | 193 | ); |
| | 194 | errdefer self.allocator.free(self.os_data.eventfd_resume_nodes); |
| | 195 | |
| | 196 | errdefer { |
| | 197 | while (self.os_data.available_eventfd_resume_nodes.pop()) |node| std.os.close(node.data.eventfd); |
| | 198 | } |
| | 199 | for (self.os_data.eventfd_resume_nodes) |*eventfd_node| { |
| | 200 | eventfd_node.* = std.atomic.Stack(ResumeNode.EventFd).Node{ |
| | 201 | .data = ResumeNode.EventFd{ |
| | 202 | .base = ResumeNode{ |
| | 203 | .id = ResumeNode.Id.EventFd, |
| | 204 | .handle = undefined, |
| | 205 | }, |
| | 206 | .eventfd = try std.os.linuxEventFd(1, posix.EFD_CLOEXEC | posix.EFD_NONBLOCK), |
| | 207 | }, |
| | 208 | .next = undefined, |
| | 209 | }; |
| | 210 | self.os_data.available_eventfd_resume_nodes.push(eventfd_node); |
| | 211 | } |
| | 212 | |
| | 213 | self.os_data.epollfd = try std.os.linuxEpollCreate(posix.EPOLL_CLOEXEC); |
| 137 | errdefer std.os.close(self.os_data.epollfd); | 214 | errdefer std.os.close(self.os_data.epollfd); |
| | 215 | |
| | 216 | self.os_data.final_eventfd = try std.os.linuxEventFd(0, posix.EFD_CLOEXEC | posix.EFD_NONBLOCK); |
| | 217 | errdefer std.os.close(self.os_data.final_eventfd); |
| | 218 | |
| | 219 | self.os_data.final_eventfd_event = posix.epoll_event{ |
| | 220 | .events = posix.EPOLLIN, |
| | 221 | .data = posix.epoll_data{ .ptr = @ptrToInt(&self.final_resume_node) }, |
| | 222 | }; |
| | 223 | try std.os.linuxEpollCtl( |
| | 224 | self.os_data.epollfd, |
| | 225 | posix.EPOLL_CTL_ADD, |
| | 226 | self.os_data.final_eventfd, |
| | 227 | &self.os_data.final_eventfd_event, |
| | 228 | ); |
| | 229 | self.extra_threads = try self.allocator.alloc(*std.os.Thread, extra_thread_count); |
| | 230 | errdefer self.allocator.free(self.extra_threads); |
| | 231 | |
| | 232 | var extra_thread_index: usize = 0; |
| | 233 | errdefer { |
| | 234 | while (extra_thread_index != 0) { |
| | 235 | extra_thread_index -= 1; |
| | 236 | // writing 8 bytes to an eventfd cannot fail |
| | 237 | std.os.posixWrite(self.os_data.final_eventfd, wakeup_bytes) catch unreachable; |
| | 238 | self.extra_threads[extra_thread_index].wait(); |
| | 239 | } |
| | 240 | } |
| | 241 | while (extra_thread_index < extra_thread_count) : (extra_thread_index += 1) { |
| | 242 | self.extra_threads[extra_thread_index] = try std.os.spawnThread(self, workerRun); |
| | 243 | } |
| 138 | }, | 244 | }, |
| 139 | else => {}, | 245 | else => {}, |
| 140 | } | 246 | } |
| ... | @@ -142,65 +248,154 @@ pub const Loop = struct { | ... | @@ -142,65 +248,154 @@ pub const Loop = struct { |
| 142 | | 248 | |
| 143 | fn deinitOsData(self: *Loop) void { | 249 | fn deinitOsData(self: *Loop) void { |
| 144 | switch (builtin.os) { | 250 | switch (builtin.os) { |
| 145 | builtin.Os.linux => std.os.close(self.os_data.epollfd), | 251 | builtin.Os.linux => { |
| | 252 | std.os.close(self.os_data.final_eventfd); |
| | 253 | while (self.os_data.available_eventfd_resume_nodes.pop()) |node| std.os.close(node.data.eventfd); |
| | 254 | std.os.close(self.os_data.epollfd); |
| | 255 | self.allocator.free(self.os_data.eventfd_resume_nodes); |
| | 256 | self.allocator.free(self.extra_threads); |
| | 257 | }, |
| 146 | else => {}, | 258 | else => {}, |
| 147 | } | 259 | } |
| 148 | } | 260 | } |
| 149 | | 261 | |
| 150 | pub fn addFd(self: *Loop, fd: i32, prom: promise) !void { | 262 | /// resume_node must live longer than the promise that it holds a reference to. |
| | 263 | pub fn addFd(self: *Loop, fd: i32, resume_node: *ResumeNode) !void { |
| | 264 | _ = @atomicRmw(usize, &self.pending_event_count, AtomicRmwOp.Add, 1, AtomicOrder.SeqCst); |
| | 265 | errdefer { |
| | 266 | _ = @atomicRmw(usize, &self.pending_event_count, AtomicRmwOp.Sub, 1, AtomicOrder.SeqCst); |
| | 267 | } |
| | 268 | try self.addFdNoCounter(fd, resume_node); |
| | 269 | } |
| | 270 | |
| | 271 | fn addFdNoCounter(self: *Loop, fd: i32, resume_node: *ResumeNode) !void { |
| 151 | var ev = std.os.linux.epoll_event{ | 272 | var ev = std.os.linux.epoll_event{ |
| 152 | .events = std.os.linux.EPOLLIN | std.os.linux.EPOLLOUT | std.os.linux.EPOLLET, | 273 | .events = std.os.linux.EPOLLIN | std.os.linux.EPOLLOUT | std.os.linux.EPOLLET, |
| 153 | .data = std.os.linux.epoll_data{ .ptr = @ptrToInt(prom) }, | 274 | .data = std.os.linux.epoll_data{ .ptr = @ptrToInt(resume_node) }, |
| 154 | }; | 275 | }; |
| 155 | try std.os.linuxEpollCtl(self.os_data.epollfd, std.os.linux.EPOLL_CTL_ADD, fd, &ev); | 276 | try std.os.linuxEpollCtl(self.os_data.epollfd, std.os.linux.EPOLL_CTL_ADD, fd, &ev); |
| 156 | } | 277 | } |
| 157 | | 278 | |
| 158 | pub fn removeFd(self: *Loop, fd: i32) void { | 279 | pub fn removeFd(self: *Loop, fd: i32) void { |
| | 280 | self.removeFdNoCounter(fd); |
| | 281 | _ = @atomicRmw(usize, &self.pending_event_count, AtomicRmwOp.Sub, 1, AtomicOrder.SeqCst); |
| | 282 | } |
| | 283 | |
| | 284 | fn removeFdNoCounter(self: *Loop, fd: i32) void { |
| 159 | std.os.linuxEpollCtl(self.os_data.epollfd, std.os.linux.EPOLL_CTL_DEL, fd, undefined) catch {}; | 285 | std.os.linuxEpollCtl(self.os_data.epollfd, std.os.linux.EPOLL_CTL_DEL, fd, undefined) catch {}; |
| 160 | } | 286 | } |
| 161 | async fn waitFd(self: *Loop, fd: i32) !void { | 287 | |
| | 288 | pub async fn waitFd(self: *Loop, fd: i32) !void { |
| 162 | defer self.removeFd(fd); | 289 | defer self.removeFd(fd); |
| | 290 | var resume_node = ResumeNode{ |
| | 291 | .id = ResumeNode.Id.Basic, |
| | 292 | .handle = undefined, |
| | 293 | }; |
| 163 | suspend |p| { | 294 | suspend |p| { |
| 164 | try self.addFd(fd, p); | 295 | resume_node.handle = p; |
| | 296 | try self.addFd(fd, &resume_node); |
| 165 | } | 297 | } |
| | 298 | var a = &resume_node; // TODO better way to explicitly put memory in coro frame |
| 166 | } | 299 | } |
| 167 | | 300 | |
| 168 | pub fn stop(self: *Loop) void { | 301 | /// Bring your own linked list node. This means it can't fail. |
| 169 | // TODO make atomic | | |
| 170 | self.keep_running = false; | | |
| 171 | // TODO activate an fd in the epoll set which should cancel all the promises | | |
| 172 | } | | |
| 173 | | | |
| 174 | /// bring your own linked list node. this means it can't fail. | | |
| 175 | pub fn onNextTick(self: *Loop, node: *NextTickNode) void { | 302 | pub fn onNextTick(self: *Loop, node: *NextTickNode) void { |
| | 303 | _ = @atomicRmw(usize, &self.pending_event_count, AtomicRmwOp.Add, 1, AtomicOrder.SeqCst); |
| 176 | self.next_tick_queue.put(node); | 304 | self.next_tick_queue.put(node); |
| 177 | } | 305 | } |
| 178 | | 306 | |
| 179 | pub fn run(self: *Loop) void { | 307 | pub fn run(self: *Loop) void { |
| 180 | while (self.keep_running) { | 308 | _ = @atomicRmw(u8, &self.dispatch_lock, AtomicRmwOp.Xchg, 0, AtomicOrder.SeqCst); |
| 181 | // TODO multiplex the next tick queue and the epoll event results onto a thread pool | 309 | self.workerRun(); |
| 182 | while (self.next_tick_queue.get()) |node| { | 310 | for (self.extra_threads) |extra_thread| { |
| 183 | resume node.data; | 311 | extra_thread.wait(); |
| 184 | } | | |
| 185 | if (!self.keep_running) break; | | |
| 186 | | | |
| 187 | self.dispatchOsEvents(); | | |
| 188 | } | 312 | } |
| 189 | } | 313 | } |
| 190 | | 314 | |
| 191 | fn dispatchOsEvents(self: *Loop) void { | 315 | fn workerRun(self: *Loop) void { |
| 192 | switch (builtin.os) { | 316 | start_over: while (true) { |
| 193 | builtin.Os.linux => { | 317 | if (@atomicRmw(u8, &self.dispatch_lock, AtomicRmwOp.Xchg, 1, AtomicOrder.SeqCst) == 0) { |
| 194 | var events: [16]std.os.linux.epoll_event = undefined; | 318 | while (self.next_tick_queue.get()) |next_tick_node| { |
| 195 | const count = std.os.linuxEpollWait(self.os_data.epollfd, events[0..], -1); | 319 | const handle = next_tick_node.data; |
| 196 | for (events[0..count]) |ev| { | 320 | if (self.next_tick_queue.isEmpty()) { |
| 197 | const p = @intToPtr(promise, ev.data.ptr); | 321 | // last node, just resume it |
| 198 | resume p; | 322 | _ = @atomicRmw(u8, &self.dispatch_lock, AtomicRmwOp.Xchg, 0, AtomicOrder.SeqCst); |
| | 323 | resume handle; |
| | 324 | _ = @atomicRmw(usize, &self.pending_event_count, AtomicRmwOp.Sub, 1, AtomicOrder.SeqCst); |
| | 325 | continue :start_over; |
| | 326 | } |
| | 327 | |
| | 328 | // non-last node, stick it in the epoll set so that |
| | 329 | // other threads can get to it |
| | 330 | if (self.os_data.available_eventfd_resume_nodes.pop()) |resume_stack_node| { |
| | 331 | const eventfd_node = &resume_stack_node.data; |
| | 332 | eventfd_node.base.handle = handle; |
| | 333 | // the pending count is already accounted for |
| | 334 | self.addFdNoCounter(eventfd_node.eventfd, &eventfd_node.base) catch |_| { |
| | 335 | // fine, we didn't need it anyway |
| | 336 | _ = @atomicRmw(u8, &self.dispatch_lock, AtomicRmwOp.Xchg, 0, AtomicOrder.SeqCst); |
| | 337 | self.os_data.available_eventfd_resume_nodes.push(resume_stack_node); |
| | 338 | resume handle; |
| | 339 | _ = @atomicRmw(usize, &self.pending_event_count, AtomicRmwOp.Sub, 1, AtomicOrder.SeqCst); |
| | 340 | continue :start_over; |
| | 341 | }; |
| | 342 | } else { |
| | 343 | // threads are too busy, can't add another eventfd to wake one up |
| | 344 | _ = @atomicRmw(u8, &self.dispatch_lock, AtomicRmwOp.Xchg, 0, AtomicOrder.SeqCst); |
| | 345 | resume handle; |
| | 346 | _ = @atomicRmw(usize, &self.pending_event_count, AtomicRmwOp.Sub, 1, AtomicOrder.SeqCst); |
| | 347 | continue :start_over; |
| | 348 | } |
| 199 | } | 349 | } |
| 200 | }, | 350 | |
| 201 | else => {}, | 351 | const pending_event_count = @atomicLoad(usize, &self.pending_event_count, AtomicOrder.SeqCst); |
| | 352 | if (pending_event_count == 0) { |
| | 353 | // cause all the threads to stop |
| | 354 | // writing 8 bytes to an eventfd cannot fail |
| | 355 | std.os.posixWrite(self.os_data.final_eventfd, wakeup_bytes) catch unreachable; |
| | 356 | return; |
| | 357 | } |
| | 358 | |
| | 359 | _ = @atomicRmw(u8, &self.dispatch_lock, AtomicRmwOp.Xchg, 0, AtomicOrder.SeqCst); |
| | 360 | } |
| | 361 | |
| | 362 | // only process 1 event so we don't steal from other threads |
| | 363 | var events: [1]std.os.linux.epoll_event = undefined; |
| | 364 | const count = std.os.linuxEpollWait(self.os_data.epollfd, events[0..], -1); |
| | 365 | for (events[0..count]) |ev| { |
| | 366 | const resume_node = @intToPtr(*ResumeNode, ev.data.ptr); |
| | 367 | const handle = resume_node.handle; |
| | 368 | const resume_node_id = resume_node.id; |
| | 369 | switch (resume_node_id) { |
| | 370 | ResumeNode.Id.Basic => {}, |
| | 371 | ResumeNode.Id.Stop => return, |
| | 372 | ResumeNode.Id.EventFd => { |
| | 373 | const event_fd_node = @fieldParentPtr(ResumeNode.EventFd, "base", resume_node); |
| | 374 | self.removeFdNoCounter(event_fd_node.eventfd); |
| | 375 | const stack_node = @fieldParentPtr(std.atomic.Stack(ResumeNode.EventFd).Node, "data", event_fd_node); |
| | 376 | self.os_data.available_eventfd_resume_nodes.push(stack_node); |
| | 377 | }, |
| | 378 | } |
| | 379 | resume handle; |
| | 380 | if (resume_node_id == ResumeNode.Id.EventFd) { |
| | 381 | _ = @atomicRmw(usize, &self.pending_event_count, AtomicRmwOp.Sub, 1, AtomicOrder.SeqCst); |
| | 382 | } |
| | 383 | } |
| 202 | } | 384 | } |
| 203 | } | 385 | } |
| | 386 | |
| | 387 | const OsData = switch (builtin.os) { |
| | 388 | builtin.Os.linux => struct { |
| | 389 | epollfd: i32, |
| | 390 | // pre-allocated eventfds. all permanently active. |
| | 391 | // this is how we send promises to be resumed on other threads. |
| | 392 | available_eventfd_resume_nodes: std.atomic.Stack(ResumeNode.EventFd), |
| | 393 | eventfd_resume_nodes: []std.atomic.Stack(ResumeNode.EventFd).Node, |
| | 394 | final_eventfd: i32, |
| | 395 | final_eventfd_event: posix.epoll_event, |
| | 396 | }, |
| | 397 | else => struct {}, |
| | 398 | }; |
| 204 | }; | 399 | }; |
| 205 | | 400 | |
| 206 | /// many producer, many consumer, thread-safe, lock-free, runtime configurable buffer size | 401 | /// many producer, many consumer, thread-safe, lock-free, runtime configurable buffer size |
| ... | @@ -304,9 +499,7 @@ pub fn Channel(comptime T: type) type { | ... | @@ -304,9 +499,7 @@ pub fn Channel(comptime T: type) type { |
| 304 | // TODO integrate this function with named return values | 499 | // TODO integrate this function with named return values |
| 305 | // so we can get rid of this extra result copy | 500 | // so we can get rid of this extra result copy |
| 306 | var result: T = undefined; | 501 | var result: T = undefined; |
| 307 | var debug_handle: usize = undefined; | | |
| 308 | suspend |handle| { | 502 | suspend |handle| { |
| 309 | debug_handle = @ptrToInt(handle); | | |
| 310 | var my_tick_node = Loop.NextTickNode{ | 503 | var my_tick_node = Loop.NextTickNode{ |
| 311 | .next = undefined, | 504 | .next = undefined, |
| 312 | .data = handle, | 505 | .data = handle, |
| ... | @@ -438,9 +631,8 @@ test "listen on a port, send bytes, receive bytes" { | ... | @@ -438,9 +631,8 @@ test "listen on a port, send bytes, receive bytes" { |
| 438 | const self = @fieldParentPtr(Self, "tcp_server", tcp_server); | 631 | const self = @fieldParentPtr(Self, "tcp_server", tcp_server); |
| 439 | var socket = _socket.*; // TODO https://github.com/ziglang/zig/issues/733 | 632 | var socket = _socket.*; // TODO https://github.com/ziglang/zig/issues/733 |
| 440 | defer socket.close(); | 633 | defer socket.close(); |
| 441 | const next_handler = async errorableHandler(self, _addr, socket) catch |err| switch (err) { | 634 | // TODO guarantee elision of this allocation |
| 442 | error.OutOfMemory => @panic("unable to handle connection: out of memory"), | 635 | const next_handler = async errorableHandler(self, _addr, socket) catch unreachable; |
| 443 | }; | | |
| 444 | (await next_handler) catch |err| { | 636 | (await next_handler) catch |err| { |
| 445 | std.debug.panic("unable to handle connection: {}\n", err); | 637 | std.debug.panic("unable to handle connection: {}\n", err); |
| 446 | }; | 638 | }; |
| ... | @@ -461,17 +653,18 @@ test "listen on a port, send bytes, receive bytes" { | ... | @@ -461,17 +653,18 @@ test "listen on a port, send bytes, receive bytes" { |
| 461 | const ip4addr = std.net.parseIp4("127.0.0.1") catch unreachable; | 653 | const ip4addr = std.net.parseIp4("127.0.0.1") catch unreachable; |
| 462 | const addr = std.net.Address.initIp4(ip4addr, 0); | 654 | const addr = std.net.Address.initIp4(ip4addr, 0); |
| 463 | | 655 | |
| 464 | var loop = try Loop.init(std.debug.global_allocator); | 656 | var loop: Loop = undefined; |
| 465 | var server = MyServer{ .tcp_server = try TcpServer.init(&loop) }; | 657 | try loop.initSingleThreaded(std.debug.global_allocator); |
| | 658 | var server = MyServer{ .tcp_server = TcpServer.init(&loop) }; |
| 466 | defer server.tcp_server.deinit(); | 659 | defer server.tcp_server.deinit(); |
| 467 | try server.tcp_server.listen(addr, MyServer.handler); | 660 | try server.tcp_server.listen(addr, MyServer.handler); |
| 468 | | 661 | |
| 469 | const p = try async<std.debug.global_allocator> doAsyncTest(&loop, server.tcp_server.listen_address); | 662 | const p = try async<std.debug.global_allocator> doAsyncTest(&loop, server.tcp_server.listen_address, &server.tcp_server); |
| 470 | defer cancel p; | 663 | defer cancel p; |
| 471 | loop.run(); | 664 | loop.run(); |
| 472 | } | 665 | } |
| 473 | | 666 | |
| 474 | async fn doAsyncTest(loop: *Loop, address: *const std.net.Address) void { | 667 | async fn doAsyncTest(loop: *Loop, address: *const std.net.Address, server: *TcpServer) void { |
| 475 | errdefer @panic("test failure"); | 668 | errdefer @panic("test failure"); |
| 476 | | 669 | |
| 477 | var socket_file = try await try async event.connect(loop, address); | 670 | var socket_file = try await try async event.connect(loop, address); |
| ... | @@ -481,7 +674,7 @@ async fn doAsyncTest(loop: *Loop, address: *const std.net.Address) void { | ... | @@ -481,7 +674,7 @@ async fn doAsyncTest(loop: *Loop, address: *const std.net.Address) void { |
| 481 | const amt_read = try socket_file.read(buf[0..]); | 674 | const amt_read = try socket_file.read(buf[0..]); |
| 482 | const msg = buf[0..amt_read]; | 675 | const msg = buf[0..amt_read]; |
| 483 | assert(mem.eql(u8, msg, "hello from server\n")); | 676 | assert(mem.eql(u8, msg, "hello from server\n")); |
| 484 | loop.stop(); | 677 | server.close(); |
| 485 | } | 678 | } |
| 486 | | 679 | |
| 487 | test "std.event.Channel" { | 680 | test "std.event.Channel" { |
| ... | @@ -490,7 +683,9 @@ test "std.event.Channel" { | ... | @@ -490,7 +683,9 @@ test "std.event.Channel" { |
| 490 | | 683 | |
| 491 | const allocator = &da.allocator; | 684 | const allocator = &da.allocator; |
| 492 | | 685 | |
| 493 | var loop = try Loop.init(allocator); | 686 | var loop: Loop = undefined; |
| | 687 | // TODO make a multi threaded test |
| | 688 | try loop.initSingleThreaded(allocator); |
| 494 | defer loop.deinit(); | 689 | defer loop.deinit(); |
| 495 | | 690 | |
| 496 | const channel = try Channel(i32).create(&loop, 0); | 691 | const channel = try Channel(i32).create(&loop, 0); |
| ... | @@ -515,11 +710,248 @@ async fn testChannelGetter(loop: *Loop, channel: *Channel(i32)) void { | ... | @@ -515,11 +710,248 @@ async fn testChannelGetter(loop: *Loop, channel: *Channel(i32)) void { |
| 515 | const value2_promise = try async channel.get(); | 710 | const value2_promise = try async channel.get(); |
| 516 | const value2 = await value2_promise; | 711 | const value2 = await value2_promise; |
| 517 | assert(value2 == 4567); | 712 | assert(value2 == 4567); |
| 518 | | | |
| 519 | loop.stop(); | | |
| 520 | } | 713 | } |
| 521 | | 714 | |
| 522 | async fn testChannelPutter(channel: *Channel(i32)) void { | 715 | async fn testChannelPutter(channel: *Channel(i32)) void { |
| 523 | await (async channel.put(1234) catch @panic("out of memory")); | 716 | await (async channel.put(1234) catch @panic("out of memory")); |
| 524 | await (async channel.put(4567) catch @panic("out of memory")); | 717 | await (async channel.put(4567) catch @panic("out of memory")); |
| 525 | } | 718 | } |
| | 719 | |
| | 720 | /// Thread-safe async/await lock. |
| | 721 | /// Does not make any syscalls - coroutines which are waiting for the lock are suspended, and |
| | 722 | /// are resumed when the lock is released, in order. |
| | 723 | pub const Lock = struct { |
| | 724 | loop: *Loop, |
| | 725 | shared_bit: u8, // TODO make this a bool |
| | 726 | queue: Queue, |
| | 727 | queue_empty_bit: u8, // TODO make this a bool |
| | 728 | |
| | 729 | const Queue = std.atomic.QueueMpsc(promise); |
| | 730 | |
| | 731 | pub const Held = struct { |
| | 732 | lock: *Lock, |
| | 733 | |
| | 734 | pub fn release(self: Held) void { |
| | 735 | // Resume the next item from the queue. |
| | 736 | if (self.lock.queue.get()) |node| { |
| | 737 | self.lock.loop.onNextTick(node); |
| | 738 | return; |
| | 739 | } |
| | 740 | |
| | 741 | // We need to release the lock. |
| | 742 | _ = @atomicRmw(u8, &self.lock.queue_empty_bit, AtomicRmwOp.Xchg, 1, AtomicOrder.SeqCst); |
| | 743 | _ = @atomicRmw(u8, &self.lock.shared_bit, AtomicRmwOp.Xchg, 0, AtomicOrder.SeqCst); |
| | 744 | |
| | 745 | // There might be a queue item. If we know the queue is empty, we can be done, |
| | 746 | // because the other actor will try to obtain the lock. |
| | 747 | // But if there's a queue item, we are the actor which must loop and attempt |
| | 748 | // to grab the lock again. |
| | 749 | if (@atomicLoad(u8, &self.lock.queue_empty_bit, AtomicOrder.SeqCst) == 1) { |
| | 750 | return; |
| | 751 | } |
| | 752 | |
| | 753 | while (true) { |
| | 754 | const old_bit = @atomicRmw(u8, &self.lock.shared_bit, AtomicRmwOp.Xchg, 1, AtomicOrder.SeqCst); |
| | 755 | if (old_bit != 0) { |
| | 756 | // We did not obtain the lock. Great, the queue is someone else's problem. |
| | 757 | return; |
| | 758 | } |
| | 759 | |
| | 760 | // Resume the next item from the queue. |
| | 761 | if (self.lock.queue.get()) |node| { |
| | 762 | self.lock.loop.onNextTick(node); |
| | 763 | return; |
| | 764 | } |
| | 765 | |
| | 766 | // Release the lock again. |
| | 767 | _ = @atomicRmw(u8, &self.lock.queue_empty_bit, AtomicRmwOp.Xchg, 1, AtomicOrder.SeqCst); |
| | 768 | _ = @atomicRmw(u8, &self.lock.shared_bit, AtomicRmwOp.Xchg, 0, AtomicOrder.SeqCst); |
| | 769 | |
| | 770 | // Find out if we can be done. |
| | 771 | if (@atomicLoad(u8, &self.lock.queue_empty_bit, AtomicOrder.SeqCst) == 1) { |
| | 772 | return; |
| | 773 | } |
| | 774 | } |
| | 775 | } |
| | 776 | }; |
| | 777 | |
| | 778 | pub fn init(loop: *Loop) Lock { |
| | 779 | return Lock{ |
| | 780 | .loop = loop, |
| | 781 | .shared_bit = 0, |
| | 782 | .queue = Queue.init(), |
| | 783 | .queue_empty_bit = 1, |
| | 784 | }; |
| | 785 | } |
| | 786 | |
| | 787 | /// Must be called when not locked. Not thread safe. |
| | 788 | /// All calls to acquire() and release() must complete before calling deinit(). |
| | 789 | pub fn deinit(self: *Lock) void { |
| | 790 | assert(self.shared_bit == 0); |
| | 791 | while (self.queue.get()) |node| cancel node.data; |
| | 792 | } |
| | 793 | |
| | 794 | pub async fn acquire(self: *Lock) Held { |
| | 795 | var my_tick_node: Loop.NextTickNode = undefined; |
| | 796 | |
| | 797 | s: suspend |handle| { |
| | 798 | my_tick_node.data = handle; |
| | 799 | self.queue.put(&my_tick_node); |
| | 800 | |
| | 801 | // At this point, we are in the queue, so we might have already been resumed and this coroutine |
| | 802 | // frame might be destroyed. For the rest of the suspend block we cannot access the coroutine frame. |
| | 803 | |
| | 804 | // We set this bit so that later we can rely on the fact, that if queue_empty_bit is 1, some actor |
| | 805 | // will attempt to grab the lock. |
| | 806 | _ = @atomicRmw(u8, &self.queue_empty_bit, AtomicRmwOp.Xchg, 0, AtomicOrder.SeqCst); |
| | 807 | |
| | 808 | while (true) { |
| | 809 | const old_bit = @atomicRmw(u8, &self.shared_bit, AtomicRmwOp.Xchg, 1, AtomicOrder.SeqCst); |
| | 810 | if (old_bit != 0) { |
| | 811 | // We did not obtain the lock. Trust that our queue entry will resume us, and allow |
| | 812 | // suspend to complete. |
| | 813 | break; |
| | 814 | } |
| | 815 | // We got the lock. However we might have already been resumed from the queue. |
| | 816 | if (self.queue.get()) |node| { |
| | 817 | // Whether this node is us or someone else, we tail resume it. |
| | 818 | resume node.data; |
| | 819 | break; |
| | 820 | } else { |
| | 821 | // We already got resumed, and there are none left in the queue, which means that |
| | 822 | // we aren't even supposed to hold the lock right now. |
| | 823 | _ = @atomicRmw(u8, &self.queue_empty_bit, AtomicRmwOp.Xchg, 1, AtomicOrder.SeqCst); |
| | 824 | _ = @atomicRmw(u8, &self.shared_bit, AtomicRmwOp.Xchg, 0, AtomicOrder.SeqCst); |
| | 825 | |
| | 826 | // There might be a queue item. If we know the queue is empty, we can be done, |
| | 827 | // because the other actor will try to obtain the lock. |
| | 828 | // But if there's a queue item, we are the actor which must loop and attempt |
| | 829 | // to grab the lock again. |
| | 830 | if (@atomicLoad(u8, &self.queue_empty_bit, AtomicOrder.SeqCst) == 1) { |
| | 831 | break; |
| | 832 | } else { |
| | 833 | continue; |
| | 834 | } |
| | 835 | } |
| | 836 | unreachable; |
| | 837 | } |
| | 838 | } |
| | 839 | |
| | 840 | // TODO this workaround to force my_tick_node to be in the coroutine frame should |
| | 841 | // not be necessary |
| | 842 | var trash1 = &my_tick_node; |
| | 843 | |
| | 844 | return Held{ .lock = self }; |
| | 845 | } |
| | 846 | }; |
| | 847 | |
| | 848 | /// Thread-safe async/await lock that protects one piece of data. |
| | 849 | /// Does not make any syscalls - coroutines which are waiting for the lock are suspended, and |
| | 850 | /// are resumed when the lock is released, in order. |
| | 851 | pub fn Locked(comptime T: type) type { |
| | 852 | return struct { |
| | 853 | lock: Lock, |
| | 854 | private_data: T, |
| | 855 | |
| | 856 | const Self = this; |
| | 857 | |
| | 858 | pub const HeldLock = struct { |
| | 859 | value: *T, |
| | 860 | held: Lock.Held, |
| | 861 | |
| | 862 | pub fn release(self: HeldLock) void { |
| | 863 | self.held.release(); |
| | 864 | } |
| | 865 | }; |
| | 866 | |
| | 867 | pub fn init(loop: *Loop, data: T) Self { |
| | 868 | return Self{ |
| | 869 | .lock = Lock.init(loop), |
| | 870 | .private_data = data, |
| | 871 | }; |
| | 872 | } |
| | 873 | |
| | 874 | pub fn deinit(self: *Self) void { |
| | 875 | self.lock.deinit(); |
| | 876 | } |
| | 877 | |
| | 878 | pub async fn acquire(self: *Self) HeldLock { |
| | 879 | return HeldLock{ |
| | 880 | // TODO guaranteed allocation elision |
| | 881 | .held = await (async self.lock.acquire() catch unreachable), |
| | 882 | .value = &self.private_data, |
| | 883 | }; |
| | 884 | } |
| | 885 | }; |
| | 886 | } |
| | 887 | |
| | 888 | test "std.event.Lock" { |
| | 889 | var da = std.heap.DirectAllocator.init(); |
| | 890 | defer da.deinit(); |
| | 891 | |
| | 892 | const allocator = &da.allocator; |
| | 893 | |
| | 894 | var loop: Loop = undefined; |
| | 895 | try loop.initMultiThreaded(allocator); |
| | 896 | defer loop.deinit(); |
| | 897 | |
| | 898 | var lock = Lock.init(&loop); |
| | 899 | defer lock.deinit(); |
| | 900 | |
| | 901 | const handle = try async<allocator> testLock(&loop, &lock); |
| | 902 | defer cancel handle; |
| | 903 | loop.run(); |
| | 904 | |
| | 905 | assert(mem.eql(i32, shared_test_data, [1]i32{3 * 10} ** 10)); |
| | 906 | } |
| | 907 | |
| | 908 | async fn testLock(loop: *Loop, lock: *Lock) void { |
| | 909 | const handle1 = async lockRunner(lock) catch @panic("out of memory"); |
| | 910 | var tick_node1 = Loop.NextTickNode{ |
| | 911 | .next = undefined, |
| | 912 | .data = handle1, |
| | 913 | }; |
| | 914 | loop.onNextTick(&tick_node1); |
| | 915 | |
| | 916 | const handle2 = async lockRunner(lock) catch @panic("out of memory"); |
| | 917 | var tick_node2 = Loop.NextTickNode{ |
| | 918 | .next = undefined, |
| | 919 | .data = handle2, |
| | 920 | }; |
| | 921 | loop.onNextTick(&tick_node2); |
| | 922 | |
| | 923 | const handle3 = async lockRunner(lock) catch @panic("out of memory"); |
| | 924 | var tick_node3 = Loop.NextTickNode{ |
| | 925 | .next = undefined, |
| | 926 | .data = handle3, |
| | 927 | }; |
| | 928 | loop.onNextTick(&tick_node3); |
| | 929 | |
| | 930 | await handle1; |
| | 931 | await handle2; |
| | 932 | await handle3; |
| | 933 | |
| | 934 | // TODO this is to force tick node memory to be in the coro frame |
| | 935 | // there should be a way to make it explicit where the memory is |
| | 936 | var a = &tick_node1; |
| | 937 | var b = &tick_node2; |
| | 938 | var c = &tick_node3; |
| | 939 | } |
| | 940 | |
| | 941 | var shared_test_data = [1]i32{0} ** 10; |
| | 942 | var shared_test_index: usize = 0; |
| | 943 | |
| | 944 | async fn lockRunner(lock: *Lock) void { |
| | 945 | suspend; // resumed by onNextTick |
| | 946 | |
| | 947 | var i: usize = 0; |
| | 948 | while (i < 10) : (i += 1) { |
| | 949 | const handle = await (async lock.acquire() catch @panic("out of memory")); |
| | 950 | defer handle.release(); |
| | 951 | |
| | 952 | shared_test_index = 0; |
| | 953 | while (shared_test_index < shared_test_data.len) : (shared_test_index += 1) { |
| | 954 | shared_test_data[shared_test_index] = shared_test_data[shared_test_index] + 1; |
| | 955 | } |
| | 956 | } |
| | 957 | } |