| ... | ... | @@ -11,53 +11,69 @@ pub const TcpServer = struct { |
| 11 | 11 | handleRequestFn: async<*mem.Allocator> fn (*TcpServer, *const std.net.Address, *const std.os.File) void, |
| 12 | 12 | |
| 13 | 13 | loop: *Loop, |
| 14 | | sockfd: i32, |
| 14 | sockfd: ?i32, |
| 15 | 15 | accept_coro: ?promise, |
| 16 | 16 | listen_address: std.net.Address, |
| 17 | 17 | |
| 18 | 18 | waiting_for_emfile_node: PromiseNode, |
| 19 | listen_resume_node: event.Loop.ResumeNode, |
| 19 | 20 | |
| 20 | 21 | const PromiseNode = std.LinkedList(promise).Node; |
| 21 | 22 | |
| 22 | | 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 | | |
| 23 | pub fn init(loop: *Loop) TcpServer { |
| 26 | 24 | // TODO can't initialize handler coroutine here because we need well defined copy elision |
| 27 | 25 | return TcpServer{ |
| 28 | 26 | .loop = loop, |
| 29 | | .sockfd = sockfd, |
| 27 | .sockfd = null, |
| 30 | 28 | .accept_coro = null, |
| 31 | 29 | .handleRequestFn = undefined, |
| 32 | 30 | .waiting_for_emfile_node = undefined, |
| 33 | 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 | 44 | self.handleRequestFn = handleRequestFn; |
| 39 | 45 | |
| 40 | | try std.os.posixBind(self.sockfd, &address.os_addr); |
| 41 | | try std.os.posixListen(self.sockfd, posix.SOMAXCONN); |
| 42 | | self.listen_address = std.net.Address.initPosix(try std.os.posixGetSockName(self.sockfd)); |
| 46 | const sockfd = try std.os.posixSocket(posix.AF_INET, posix.SOCK_STREAM | posix.SOCK_CLOEXEC | posix.SOCK_NONBLOCK, posix.PROTO_tcp); |
| 47 | errdefer std.os.close(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 | 54 | self.accept_coro = try async<self.loop.allocator> TcpServer.handler(self); |
| 45 | 55 | errdefer cancel self.accept_coro.?; |
| 46 | 56 | |
| 47 | | try self.loop.addFd(self.sockfd, self.accept_coro.?); |
| 48 | | errdefer self.loop.removeFd(self.sockfd); |
| 57 | self.listen_resume_node.handle = self.accept_coro.?; |
| 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 | 68 | pub fn deinit(self: *TcpServer) void { |
| 52 | | self.loop.removeFd(self.sockfd); |
| 53 | 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 | 73 | pub async fn handler(self: *TcpServer) void { |
| 58 | 74 | while (true) { |
| 59 | 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 | 77 | var socket = std.os.File.openHandle(accepted_fd); |
| 62 | 78 | _ = async<self.loop.allocator> self.handleRequestFn(self, accepted_addr, socket) catch |err| switch (err) { |
| 63 | 79 | error.OutOfMemory => { |
| ... | ... | @@ -95,32 +111,65 @@ pub const TcpServer = struct { |
| 95 | 111 | |
| 96 | 112 | pub const Loop = struct { |
| 97 | 113 | allocator: *mem.Allocator, |
| 98 | | keep_running: bool, |
| 99 | 114 | next_tick_queue: std.atomic.QueueMpsc(promise), |
| 100 | 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) { |
| 103 | | builtin.Os.linux => struct { |
| 104 | | epollfd: i32, |
| 105 | | }, |
| 106 | | else => struct {}, |
| 121 | pub const NextTickNode = std.atomic.QueueMpsc(promise).Node; |
| 122 | |
| 123 | pub const ResumeNode = struct { |
| 124 | id: Id, |
| 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 | 146 | /// The allocator must be thread-safe because we use it for multiplexing |
| 112 | 147 | /// coroutines onto kernel threads. |
| 113 | | pub fn init(allocator: *mem.Allocator) !Loop { |
| 114 | | var self = Loop{ |
| 115 | | .keep_running = true, |
| 148 | /// After initialization, call run(). |
| 149 | /// TODO copy elision / named return values so that the threads referencing *Loop |
| 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 | 161 | .allocator = allocator, |
| 117 | 162 | .os_data = undefined, |
| 118 | 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 | 172 | errdefer self.deinitOsData(); |
| 122 | | |
| 123 | | return self; |
| 124 | 173 | } |
| 125 | 174 | |
| 126 | 175 | /// must call stop before deinit |
| ... | ... | @@ -128,13 +177,70 @@ pub const Loop = struct { |
| 128 | 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 | 186 | switch (builtin.os) { |
| 135 | 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 | 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 | 245 | else => {}, |
| 140 | 246 | } |
| ... | ... | @@ -142,65 +248,154 @@ pub const Loop = struct { |
| 142 | 248 | |
| 143 | 249 | fn deinitOsData(self: *Loop) void { |
| 144 | 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 | 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 | 272 | var ev = std.os.linux.epoll_event{ |
| 152 | 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 | 276 | try std.os.linuxEpollCtl(self.os_data.epollfd, std.os.linux.EPOLL_CTL_ADD, fd, &ev); |
| 156 | 277 | } |
| 157 | 278 | |
| 158 | 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 | 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 | 289 | defer self.removeFd(fd); |
| 290 | var resume_node = ResumeNode{ |
| 291 | .id = ResumeNode.Id.Basic, |
| 292 | .handle = undefined, |
| 293 | }; |
| 163 | 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 { |
| 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. |
| 301 | /// Bring your own linked list node. This means it can't fail. |
| 175 | 302 | pub fn onNextTick(self: *Loop, node: *NextTickNode) void { |
| 303 | _ = @atomicRmw(usize, &self.pending_event_count, AtomicRmwOp.Add, 1, AtomicOrder.SeqCst); |
| 176 | 304 | self.next_tick_queue.put(node); |
| 177 | 305 | } |
| 178 | 306 | |
| 179 | 307 | pub fn run(self: *Loop) void { |
| 180 | | while (self.keep_running) { |
| 181 | | // TODO multiplex the next tick queue and the epoll event results onto a thread pool |
| 182 | | while (self.next_tick_queue.get()) |node| { |
| 183 | | resume node.data; |
| 184 | | } |
| 185 | | if (!self.keep_running) break; |
| 186 | | |
| 187 | | self.dispatchOsEvents(); |
| 308 | _ = @atomicRmw(u8, &self.dispatch_lock, AtomicRmwOp.Xchg, 0, AtomicOrder.SeqCst); |
| 309 | self.workerRun(); |
| 310 | for (self.extra_threads) |extra_thread| { |
| 311 | extra_thread.wait(); |
| 188 | 312 | } |
| 189 | 313 | } |
| 190 | 314 | |
| 191 | | fn dispatchOsEvents(self: *Loop) void { |
| 192 | | switch (builtin.os) { |
| 193 | | builtin.Os.linux => { |
| 194 | | var events: [16]std.os.linux.epoll_event = undefined; |
| 195 | | const count = std.os.linuxEpollWait(self.os_data.epollfd, events[0..], -1); |
| 196 | | for (events[0..count]) |ev| { |
| 197 | | const p = @intToPtr(promise, ev.data.ptr); |
| 198 | | resume p; |
| 315 | fn workerRun(self: *Loop) void { |
| 316 | start_over: while (true) { |
| 317 | if (@atomicRmw(u8, &self.dispatch_lock, AtomicRmwOp.Xchg, 1, AtomicOrder.SeqCst) == 0) { |
| 318 | while (self.next_tick_queue.get()) |next_tick_node| { |
| 319 | const handle = next_tick_node.data; |
| 320 | if (self.next_tick_queue.isEmpty()) { |
| 321 | // last node, just resume it |
| 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 | | }, |
| 201 | | else => {}, |
| 350 | |
| 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 | 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 | 499 | // TODO integrate this function with named return values |
| 305 | 500 | // so we can get rid of this extra result copy |
| 306 | 501 | var result: T = undefined; |
| 307 | | var debug_handle: usize = undefined; |
| 308 | 502 | suspend |handle| { |
| 309 | | debug_handle = @ptrToInt(handle); |
| 310 | 503 | var my_tick_node = Loop.NextTickNode{ |
| 311 | 504 | .next = undefined, |
| 312 | 505 | .data = handle, |
| ... | ... | @@ -438,9 +631,8 @@ test "listen on a port, send bytes, receive bytes" { |
| 438 | 631 | const self = @fieldParentPtr(Self, "tcp_server", tcp_server); |
| 439 | 632 | var socket = _socket.*; // TODO https://github.com/ziglang/zig/issues/733 |
| 440 | 633 | defer socket.close(); |
| 441 | | const next_handler = async errorableHandler(self, _addr, socket) catch |err| switch (err) { |
| 442 | | error.OutOfMemory => @panic("unable to handle connection: out of memory"), |
| 443 | | }; |
| 634 | // TODO guarantee elision of this allocation |
| 635 | const next_handler = async errorableHandler(self, _addr, socket) catch unreachable; |
| 444 | 636 | (await next_handler) catch |err| { |
| 445 | 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 | 653 | const ip4addr = std.net.parseIp4("127.0.0.1") catch unreachable; |
| 462 | 654 | const addr = std.net.Address.initIp4(ip4addr, 0); |
| 463 | 655 | |
| 464 | | var loop = try Loop.init(std.debug.global_allocator); |
| 465 | | var server = MyServer{ .tcp_server = try TcpServer.init(&loop) }; |
| 656 | var loop: Loop = undefined; |
| 657 | try loop.initSingleThreaded(std.debug.global_allocator); |
| 658 | var server = MyServer{ .tcp_server = TcpServer.init(&loop) }; |
| 466 | 659 | defer server.tcp_server.deinit(); |
| 467 | 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 | 663 | defer cancel p; |
| 471 | 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 | 668 | errdefer @panic("test failure"); |
| 476 | 669 | |
| 477 | 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 | 674 | const amt_read = try socket_file.read(buf[0..]); |
| 482 | 675 | const msg = buf[0..amt_read]; |
| 483 | 676 | assert(mem.eql(u8, msg, "hello from server\n")); |
| 484 | | loop.stop(); |
| 677 | server.close(); |
| 485 | 678 | } |
| 486 | 679 | |
| 487 | 680 | test "std.event.Channel" { |
| ... | ... | @@ -490,7 +683,9 @@ test "std.event.Channel" { |
| 490 | 683 | |
| 491 | 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 | 689 | defer loop.deinit(); |
| 495 | 690 | |
| 496 | 691 | const channel = try Channel(i32).create(&loop, 0); |
| ... | ... | @@ -515,11 +710,248 @@ async fn testChannelGetter(loop: *Loop, channel: *Channel(i32)) void { |
| 515 | 710 | const value2_promise = try async channel.get(); |
| 516 | 711 | const value2 = await value2_promise; |
| 517 | 712 | assert(value2 == 4567); |
| 518 | | |
| 519 | | loop.stop(); |
| 520 | 713 | } |
| 521 | 714 | |
| 522 | 715 | async fn testChannelPutter(channel: *Channel(i32)) void { |
| 523 | 716 | await (async channel.put(1234) catch @panic("out of memory")); |
| 524 | 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 | } |