authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-11-21 12:02:59-08:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-11-21 19:54:41-08:00
logcf744aa182cf3a7c4738c43f5b1fcf6c280aa74f
treed8703b12d47b93216a8128c846a8cb33810bea8a
parent13b537d77c07101a9dcb41e0839df1213ca8dd1d

std.Io.Threaded: slightly different semantics

while still preserving the guarantee about async() being assigned a unit of concurrency (or immediately running the task), this change: * retains the error from calling getCpuCount() * spawns all threads in detached mode, using WaitGroup to join them * treats all workers the same regardless of whether they are processing concurrent or async tasks. one thread pool does all the work, while respecting async and concurrent limits.

3 files changed, 90 insertions(+), 100 deletions(-)

lib/std/Io/Threaded.zig+84-98
...@@ -22,12 +22,30 @@ mutex: std.Thread.Mutex = .{},...@@ -22,12 +22,30 @@ mutex: std.Thread.Mutex = .{},
22cond: std.Thread.Condition = .{},22cond: std.Thread.Condition = .{},
23run_queue: std.SinglyLinkedList = .{},23run_queue: std.SinglyLinkedList = .{},
24join_requested: bool = false,24join_requested: bool = false,
25threads: std.ArrayList(std.Thread),
26stack_size: usize,25stack_size: usize,
27cpu_count: usize, // 0 means no limit26/// All threads are spawned detached; this is how we wait until they all exit.
28concurrency_limit: usize, // 0 means no limit27wait_group: std.Thread.WaitGroup = .{},
29available_thread_count: usize = 0,28/// Maximum thread pool size (excluding main thread) when dispatching async
30one_shot_thread_count: usize = 0,29/// tasks. Until this limit, calls to `Io.async` when all threads are busy will
30/// cause a new thread to be spawned and permanently added to the pool. After
31/// this limit, calls to `Io.async` when all threads are busy run the task
32/// immediately.
33///
34/// Defaults to a number equal to logical CPU cores.
35async_limit: Io.Limit,
36/// Maximum thread pool size (excluding main thread) for dispatching concurrent
37/// tasks. Until this limit, calls to `Io.concurrent` will increase the thread
38/// pool size.
39///
40/// concurrent tasks. After this number, calls to `Io.concurrent` return
41/// `error.ConcurrencyUnavailable`.
42concurrent_limit: Io.Limit = .unlimited,
43/// Error from calling `std.Thread.getCpuCount` in `init`.
44cpu_count_error: ?std.Thread.CpuCountError,
45/// Number of threads that are unavailable to take tasks. To calculate
46/// available count, subtract this from either `async_limit` or
47/// `concurrent_limit`.
48busy_count: usize = 0,
3149
32wsa: if (is_windows) Wsa else struct {} = .{},50wsa: if (is_windows) Wsa else struct {} = .{},
3351
...@@ -103,19 +121,18 @@ pub fn init(...@@ -103,19 +121,18 @@ pub fn init(
103) Threaded {121) Threaded {
104 if (builtin.single_threaded) return .init_single_threaded;122 if (builtin.single_threaded) return .init_single_threaded;
105123
124 const cpu_count = std.Thread.getCpuCount();
125
106 var t: Threaded = .{126 var t: Threaded = .{
107 .allocator = gpa,127 .allocator = gpa,
108 .threads = .empty,
109 .stack_size = std.Thread.SpawnConfig.default_stack_size,128 .stack_size = std.Thread.SpawnConfig.default_stack_size,
110 .cpu_count = std.Thread.getCpuCount() catch 0,129 .async_limit = if (cpu_count) |n| .limited(n - 1) else |_| .nothing,
111 .concurrency_limit = 0,130 .cpu_count_error = if (cpu_count) |_| null else |e| e,
112 .old_sig_io = undefined,131 .old_sig_io = undefined,
113 .old_sig_pipe = undefined,132 .old_sig_pipe = undefined,
114 .have_signal_handler = false,133 .have_signal_handler = false,
115 };134 };
116135
117 t.threads.ensureTotalCapacity(gpa, t.cpu_count) catch {};
118
119 if (posix.Sigaction != void) {136 if (posix.Sigaction != void) {
120 // This causes sending `posix.SIG.IO` to thread to interrupt blocking137 // This causes sending `posix.SIG.IO` to thread to interrupt blocking
121 // syscalls, returning `posix.E.INTR`.138 // syscalls, returning `posix.E.INTR`.
...@@ -140,19 +157,17 @@ pub fn init(...@@ -140,19 +157,17 @@ pub fn init(
140/// * `deinit` is safe, but unnecessary to call.157/// * `deinit` is safe, but unnecessary to call.
141pub const init_single_threaded: Threaded = .{158pub const init_single_threaded: Threaded = .{
142 .allocator = .failing,159 .allocator = .failing,
143 .threads = .empty,
144 .stack_size = std.Thread.SpawnConfig.default_stack_size,160 .stack_size = std.Thread.SpawnConfig.default_stack_size,
145 .cpu_count = 1,161 .async_limit = .nothing,
146 .concurrency_limit = 0,162 .cpu_count_error = null,
163 .concurrent_limit = .nothing,
147 .old_sig_io = undefined,164 .old_sig_io = undefined,
148 .old_sig_pipe = undefined,165 .old_sig_pipe = undefined,
149 .have_signal_handler = false,166 .have_signal_handler = false,
150};167};
151168
152pub fn deinit(t: *Threaded) void {169pub fn deinit(t: *Threaded) void {
153 const gpa = t.allocator;
154 t.join();170 t.join();
155 t.threads.deinit(gpa);
156 if (is_windows and t.wsa.status == .initialized) {171 if (is_windows and t.wsa.status == .initialized) {
157 if (ws2_32.WSACleanup() != 0) recoverableOsBugDetected();172 if (ws2_32.WSACleanup() != 0) recoverableOsBugDetected();
158 }173 }
...@@ -171,10 +186,12 @@ fn join(t: *Threaded) void {...@@ -171,10 +186,12 @@ fn join(t: *Threaded) void {
171 t.join_requested = true;186 t.join_requested = true;
172 }187 }
173 t.cond.broadcast();188 t.cond.broadcast();
174 for (t.threads.items) |thread| thread.join();189 t.wait_group.wait();
175}190}
176191
177fn worker(t: *Threaded) void {192fn worker(t: *Threaded) void {
193 defer t.wait_group.finish();
194
178 t.mutex.lock();195 t.mutex.lock();
179 defer t.mutex.unlock();196 defer t.mutex.unlock();
180197
...@@ -184,20 +201,13 @@ fn worker(t: *Threaded) void {...@@ -184,20 +201,13 @@ fn worker(t: *Threaded) void {
184 const closure: *Closure = @fieldParentPtr("node", closure_node);201 const closure: *Closure = @fieldParentPtr("node", closure_node);
185 closure.start(closure);202 closure.start(closure);
186 t.mutex.lock();203 t.mutex.lock();
187 t.available_thread_count += 1;204 t.busy_count -= 1;
188 }205 }
189 if (t.join_requested) break;206 if (t.join_requested) break;
190 t.cond.wait(&t.mutex);207 t.cond.wait(&t.mutex);
191 }208 }
192}209}
193210
194fn oneShotWorker(t: *Threaded, closure: *Closure) void {
195 closure.start(closure);
196 t.mutex.lock();
197 defer t.mutex.unlock();
198 t.one_shot_thread_count -= 1;
199}
200
201pub fn io(t: *Threaded) Io {211pub fn io(t: *Threaded) Io {
202 return .{212 return .{
203 .userdata = t,213 .userdata = t,
...@@ -488,7 +498,7 @@ fn async(...@@ -488,7 +498,7 @@ fn async(
488 start: *const fn (context: *const anyopaque, result: *anyopaque) void,498 start: *const fn (context: *const anyopaque, result: *anyopaque) void,
489) ?*Io.AnyFuture {499) ?*Io.AnyFuture {
490 const t: *Threaded = @ptrCast(@alignCast(userdata));500 const t: *Threaded = @ptrCast(@alignCast(userdata));
491 if (t.cpu_count == 1 or builtin.single_threaded) {501 if (builtin.single_threaded or t.async_limit == .nothing) {
492 start(context.ptr, result.ptr);502 start(context.ptr, result.ptr);
493 return null;503 return null;
494 }504 }
...@@ -500,35 +510,29 @@ fn async(...@@ -500,35 +510,29 @@ fn async(
500510
501 t.mutex.lock();511 t.mutex.lock();
502512
503 if (t.available_thread_count == 0) {513 const busy_count = t.busy_count;
504 if (t.cpu_count != 0 and t.threads.items.len >= t.cpu_count) {
505 t.mutex.unlock();
506 ac.deinit(gpa);
507 start(context.ptr, result.ptr);
508 return null;
509 }
510514
511 t.threads.ensureUnusedCapacity(gpa, 1) catch {515 if (busy_count >= @intFromEnum(t.async_limit)) {
512 t.mutex.unlock();516 t.mutex.unlock();
513 ac.deinit(gpa);517 ac.deinit(gpa);
514 start(context.ptr, result.ptr);518 start(context.ptr, result.ptr);
515 return null;519 return null;
516 };520 }
517521
518 const thread = std.Thread.spawn(522 t.busy_count = busy_count + 1;
519 .{ .stack_size = t.stack_size },523
520 worker,524 const pool_size = t.wait_group.value();
521 .{t},525 if (pool_size - busy_count == 0) {
522 ) catch {526 t.wait_group.start();
527 const thread = std.Thread.spawn(.{ .stack_size = t.stack_size }, worker, .{t}) catch {
528 t.wait_group.finish();
529 t.busy_count = busy_count;
523 t.mutex.unlock();530 t.mutex.unlock();
524 ac.deinit(gpa);531 ac.deinit(gpa);
525 start(context.ptr, result.ptr);532 start(context.ptr, result.ptr);
526 return null;533 return null;
527 };534 };
528535 thread.detach();
529 t.threads.appendAssumeCapacity(thread);
530 } else {
531 t.available_thread_count -= 1;
532 }536 }
533537
534 t.run_queue.prepend(&ac.closure.node);538 t.run_queue.prepend(&ac.closure.node);
...@@ -550,47 +554,33 @@ fn concurrent(...@@ -550,47 +554,33 @@ fn concurrent(
550 const t: *Threaded = @ptrCast(@alignCast(userdata));554 const t: *Threaded = @ptrCast(@alignCast(userdata));
551555
552 const gpa = t.allocator;556 const gpa = t.allocator;
553 const ac = AsyncClosure.init(gpa, result_len, result_alignment, context, context_alignment, start) catch {557 const ac = AsyncClosure.init(gpa, result_len, result_alignment, context, context_alignment, start) catch
554 return error.ConcurrencyUnavailable;558 return error.ConcurrencyUnavailable;
555 };
556 errdefer ac.deinit(gpa);559 errdefer ac.deinit(gpa);
557560
558 t.mutex.lock();561 t.mutex.lock();
559 defer t.mutex.unlock();562 defer t.mutex.unlock();
560563
561 // If there's an avilable thread, use it.564 const busy_count = t.busy_count;
562 if (t.available_thread_count > 0) {
563 t.available_thread_count -= 1;
564 t.run_queue.prepend(&ac.closure.node);
565 t.cond.signal();
566 return @ptrCast(ac);
567 }
568565
569 // If we can spawn a normal worker, spawn it and use it.566 if (busy_count >= @intFromEnum(t.concurrent_limit))
570 if (t.cpu_count == 0 or t.threads.items.len < t.cpu_count) {567 return error.ConcurrencyUnavailable;
571 t.threads.ensureUnusedCapacity(gpa, 1) catch return error.ConcurrencyUnavailable;568
569 t.busy_count = busy_count + 1;
570 errdefer t.busy_count = busy_count;
571
572 const pool_size = t.wait_group.value();
573 if (pool_size - busy_count == 0) {
574 t.wait_group.start();
575 errdefer t.wait_group.finish();
572576
573 const thread = std.Thread.spawn(.{ .stack_size = t.stack_size }, worker, .{t}) catch577 const thread = std.Thread.spawn(.{ .stack_size = t.stack_size }, worker, .{t}) catch
574 return error.ConcurrencyUnavailable;578 return error.ConcurrencyUnavailable;
575579 thread.detach();
576 t.threads.appendAssumeCapacity(thread);
577 t.run_queue.prepend(&ac.closure.node);
578 t.cond.signal();
579 return @ptrCast(ac);
580 }580 }
581581
582 // If we have a concurrencty limit and we havent' hit it yet,582 t.run_queue.prepend(&ac.closure.node);
583 // spawn a new one-shot thread.583 t.cond.signal();
584 if (t.concurrency_limit != 0 and t.one_shot_thread_count >= t.concurrency_limit)
585 return error.ConcurrencyUnavailable;
586
587 t.one_shot_thread_count += 1;
588 errdefer t.one_shot_thread_count -= 1;
589
590 const thread = std.Thread.spawn(.{ .stack_size = t.stack_size }, oneShotWorker, .{ t, &ac.closure }) catch
591 return error.ConcurrencyUnavailable;
592 thread.detach();
593
594 return @ptrCast(ac);584 return @ptrCast(ac);
595}585}
596586
...@@ -684,41 +674,37 @@ fn groupAsync(...@@ -684,41 +674,37 @@ fn groupAsync(
684 context_alignment: std.mem.Alignment,674 context_alignment: std.mem.Alignment,
685 start: *const fn (*Io.Group, context: *const anyopaque) void,675 start: *const fn (*Io.Group, context: *const anyopaque) void,
686) void {676) void {
687 if (builtin.single_threaded) return start(group, context.ptr);
688
689 const t: *Threaded = @ptrCast(@alignCast(userdata));677 const t: *Threaded = @ptrCast(@alignCast(userdata));
678 if (builtin.single_threaded or t.async_limit == .nothing)
679 return start(group, context.ptr);
680
690 const gpa = t.allocator;681 const gpa = t.allocator;
691 const gc = GroupClosure.init(gpa, t, group, context, context_alignment, start) catch682 const gc = GroupClosure.init(gpa, t, group, context, context_alignment, start) catch
692 return start(group, context.ptr);683 return start(group, context.ptr);
693684
694 t.mutex.lock();685 t.mutex.lock();
695686
696 if (t.available_thread_count == 0) {687 const busy_count = t.busy_count;
697 if (t.cpu_count != 0 and t.threads.items.len >= t.cpu_count) {
698 t.mutex.unlock();
699 gc.deinit(gpa);
700 return start(group, context.ptr);
701 }
702688
703 t.threads.ensureUnusedCapacity(gpa, 1) catch {689 if (busy_count >= @intFromEnum(t.async_limit)) {
704 t.mutex.unlock();690 t.mutex.unlock();
705 gc.deinit(gpa);691 gc.deinit(gpa);
706 return start(group, context.ptr);692 return start(group, context.ptr);
707 };693 }
708694
709 const thread = std.Thread.spawn(695 t.busy_count = busy_count + 1;
710 .{ .stack_size = t.stack_size },696
711 worker,697 const pool_size = t.wait_group.value();
712 .{t},698 if (pool_size - busy_count == 0) {
713 ) catch {699 t.wait_group.start();
700 const thread = std.Thread.spawn(.{ .stack_size = t.stack_size }, worker, .{t}) catch {
701 t.wait_group.finish();
702 t.busy_count = busy_count;
714 t.mutex.unlock();703 t.mutex.unlock();
715 gc.deinit(gpa);704 gc.deinit(gpa);
716 return start(group, context.ptr);705 return start(group, context.ptr);
717 };706 };
718707 thread.detach();
719 t.threads.appendAssumeCapacity(thread);
720 } else {
721 t.available_thread_count -= 1;
722 }708 }
723709
724 // Append to the group linked list inside the mutex to make `Io.Group.async` thread-safe.710 // Append to the group linked list inside the mutex to make `Io.Group.async` thread-safe.
lib/std/Io/Threaded/test.zig+2-2
...@@ -10,7 +10,7 @@ test "concurrent vs main prevents deadlock via oversubscription" {...@@ -10,7 +10,7 @@ test "concurrent vs main prevents deadlock via oversubscription" {
10 defer threaded.deinit();10 defer threaded.deinit();
11 const io = threaded.io();11 const io = threaded.io();
1212
13 threaded.cpu_count = 1;13 threaded.async_limit = .nothing;
1414
15 var queue: Io.Queue(u8) = .init(&.{});15 var queue: Io.Queue(u8) = .init(&.{});
1616
...@@ -38,7 +38,7 @@ test "concurrent vs concurrent prevents deadlock via oversubscription" {...@@ -38,7 +38,7 @@ test "concurrent vs concurrent prevents deadlock via oversubscription" {
38 defer threaded.deinit();38 defer threaded.deinit();
39 const io = threaded.io();39 const io = threaded.io();
4040
41 threaded.cpu_count = 1;41 threaded.async_limit = .nothing;
4242
43 var queue: Io.Queue(u8) = .init(&.{});43 var queue: Io.Queue(u8) = .init(&.{});
4444
lib/std/Thread/WaitGroup.zig+4
...@@ -60,6 +60,10 @@ pub fn isDone(wg: *WaitGroup) bool {...@@ -60,6 +60,10 @@ pub fn isDone(wg: *WaitGroup) bool {
60 return (state / one_pending) == 0;60 return (state / one_pending) == 0;
61}61}
6262
63pub fn value(wg: *WaitGroup) usize {
64 return wg.state.load(.monotonic) / one_pending;
65}
66
63// Spawns a new thread for the task. This is appropriate when the callee67// Spawns a new thread for the task. This is appropriate when the callee
64// delegates all work.68// delegates all work.
65pub fn spawnManager(69pub fn spawnManager(