websocket
0

Configure Feed

Select the types of activity you want to include in your feed.

websocket.zig / src / server / thread_pool.zig
7.3 kB 233 lines
1const std = @import("std"); 2const c = std.c; 3 4const Io = std.Io; 5const Thread = std.Thread; 6const Allocator = std.mem.Allocator; 7 8// nanosleep via libc — no Io-based uncancelable sleep exists 9fn sleep(ns: u64) void { 10 const secs = ns / std.time.ns_per_s; 11 const nsecs = ns % std.time.ns_per_s; 12 var ts: c.timespec = .{ 13 .sec = @intCast(secs), 14 .nsec = @intCast(nsecs), 15 }; 16 _ = c.nanosleep(&ts, null); 17} 18 19pub const Opts = struct { 20 count: u16, 21 backlog: u32, 22 buffer_size: usize, 23}; 24 25pub fn ThreadPool(comptime F: anytype) type { 26 // When the worker thread calls F, it'll inject its static buffer. 27 // So F would be: handle(server: *Server, conn: *Conn, buf: []u8) 28 // and FullArgs would be our 3 args.... 29 const FullArgs = std.meta.ArgsTuple(@TypeOf(F)); 30 const full_fields = std.meta.fields(FullArgs); 31 const ARG_COUNT = full_fields.len - 1; 32 33 // Args will be FullArgs[0..len-1], so in the above example, args would be 34 // (*Server, *Conn) 35 // Args is what we expect the caller to pass to spawn. The worker thread 36 // will convert an Args into FullArgs by injecting its static buffer as 37 // the final argument. 38 39 // TODO: We could verify that the last argument to FullArgs is, in fact, a 40 // []u8. But this ThreadPool is private and being used for 2 specific cases 41 // that we control. 42 43 // 0.16: @Type was removed, use @Tuple instead 44 var field_types: [ARG_COUNT]type = undefined; 45 inline for (full_fields[0..ARG_COUNT], 0..) |field, index| { 46 field_types[index] = field.type; 47 } 48 const Args = @Tuple(&field_types); 49 50 return struct { 51 stopped: bool, 52 push: usize, 53 pull: usize, 54 pending: usize, 55 queue: []Args, 56 threads: []Thread, 57 mutex: Io.Mutex, 58 pull_cond: Io.Condition, 59 push_cond: Io.Condition, 60 queue_end: usize, 61 allocator: Allocator, 62 io: Io, 63 64 const Self = @This(); 65 66 pub fn init(allocator: Allocator, opts: Opts) !*Self { 67 const queue = try allocator.alloc(Args, opts.backlog); 68 errdefer allocator.free(queue); 69 70 const threads = try allocator.alloc(Thread, opts.count); 71 errdefer allocator.free(threads); 72 73 const thread_pool = try allocator.create(Self); 74 errdefer allocator.destroy(thread_pool); 75 76 const io = std.Options.debug_io; 77 78 thread_pool.* = .{ 79 .pull = 0, 80 .push = 0, 81 .pending = 0, 82 .io = io, 83 .mutex = .init, 84 .stopped = false, 85 .queue = queue, 86 .pull_cond = .init, 87 .push_cond = .init, 88 .threads = threads, 89 .allocator = allocator, 90 .queue_end = queue.len - 1, 91 }; 92 93 var started: usize = 0; 94 errdefer { 95 thread_pool.stopped = true; 96 thread_pool.pull_cond.broadcast(io); 97 for (0..started) |i| { 98 threads[i].join(); 99 } 100 } 101 102 for (0..threads.len) |i| { 103 // This becomes owned by the thread, it'll free it as it ends 104 const buffer = try allocator.alloc(u8, opts.buffer_size); 105 errdefer allocator.free(buffer); 106 107 threads[i] = try Thread.spawn(.{}, Self.worker, .{ thread_pool, buffer }); 108 started += 1; 109 } 110 111 return thread_pool; 112 } 113 114 pub fn deinit(self: *Self) void { 115 const allocator = self.allocator; 116 self.stop(); 117 allocator.free(self.threads); 118 allocator.free(self.queue); 119 120 allocator.destroy(self); 121 } 122 123 pub fn stop(self: *Self) void { 124 const io = self.io; 125 { 126 self.mutex.lockUncancelable(io); 127 defer self.mutex.unlock(io); 128 if (self.stopped == true) { 129 return; 130 } 131 self.stopped = true; 132 } 133 134 self.pull_cond.broadcast(io); 135 for (self.threads) |thrd| { 136 thrd.join(); 137 } 138 } 139 140 pub fn empty(self: *Self) bool { 141 const io = self.io; 142 self.mutex.lockUncancelable(io); 143 defer self.mutex.unlock(io); 144 return self.pull == self.push; 145 } 146 147 pub fn spawn(self: *Self, args: Args) void { 148 const queue = self.queue; 149 const len = queue.len; 150 const io = self.io; 151 152 self.mutex.lockUncancelable(io); 153 while (self.pending == len) { 154 self.push_cond.waitUncancelable(io, &self.mutex); 155 } 156 157 const push = self.push; 158 self.queue[push] = args; 159 self.push = if (push == self.queue_end) 0 else push + 1; 160 self.pending += 1; 161 self.mutex.unlock(io); 162 163 self.pull_cond.signal(io); 164 } 165 166 fn worker(self: *Self, buffer: []u8) void { 167 defer self.allocator.free(buffer); 168 const io = self.io; 169 170 while (true) { 171 self.mutex.lockUncancelable(io); 172 while (self.pending == 0) { 173 if (self.stopped) { 174 self.mutex.unlock(io); 175 return; 176 } 177 self.pull_cond.waitUncancelable(io, &self.mutex); 178 } 179 const pull = self.pull; 180 const args = self.queue[pull]; 181 self.pull = if (pull == self.queue_end) 0 else pull + 1; 182 self.pending -= 1; 183 self.mutex.unlock(io); 184 self.push_cond.signal(io); 185 186 // convert Args to FullArgs, i.e. inject buffer as the last argument 187 var full_args: FullArgs = undefined; 188 full_args[ARG_COUNT] = buffer; 189 inline for (0..ARG_COUNT) |i| { 190 full_args[i] = args[i]; 191 } 192 @call(.auto, F, full_args); 193 } 194 } 195 }; 196} 197 198const t = @import("../t.zig"); 199test "ThreadPool: small fuzz" { 200 testSum = 0; // global defined near the end of this file 201 var tp = try ThreadPool(testIncr).init(t.allocator, .{ .count = 3, .backlog = 3, .buffer_size = 512 }); 202 203 for (0..50_000) |_| { 204 tp.spawn(.{1}); 205 } 206 while (tp.empty() == false) { 207 sleep(std.time.ns_per_ms); 208 } 209 tp.deinit(); 210 try t.expectEqual(50_000, testSum); 211} 212 213test "ThreadPool: large fuzz" { 214 testSum = 0; // global defined near the end of this file 215 var tp = try ThreadPool(testIncr).init(t.allocator, .{ .count = 50, .backlog = 1000, .buffer_size = 512 }); 216 217 for (0..50_000) |_| { 218 tp.spawn(.{1}); 219 } 220 while (tp.empty() == false) { 221 sleep(std.time.ns_per_ms); 222 } 223 tp.deinit(); 224 try t.expectEqual(50_000, testSum); 225} 226 227var testSum: u64 = 0; 228fn testIncr(val: u64, buf: []u8) void { 229 std.debug.assert(buf.len == 512); 230 _ = @atomicRmw(u64, &testSum, .Add, val, .monotonic); 231 // let the threadpool queue get backed up 232 sleep(std.time.ns_per_us * 100); 233}