authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-11-21 20:56:29-08:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2025-11-21 20:56:29-08:00
log2ea55d7153b9832e7f00c8a85ca4941ccde6b0d6
tree0b667f096647520f2c0813030c7ff8b7868fcddd
parentd828115dabf3b06711788dc2f424a1ef3cedd6a3
parent7096e66ca9b7b1e4dc7d6d5d5bf1e6833f1be039
signaturebadge-check Signed by PGP key B5690EEEBB952194

Merge pull request #25998 from ziglang/std.Io.Threaded-async-guarantee

std.Io: guarantee when async() returns, task is already completed or has been successfully assigned a unit of concurrency

5 files changed, 139 insertions(+), 118 deletions(-)

lib/std/Io.zig+16-2
...@@ -580,6 +580,9 @@ pub const VTable = struct {...@@ -580,6 +580,9 @@ pub const VTable = struct {
580 /// If it returns `null` it means `result` has been already populated and580 /// If it returns `null` it means `result` has been already populated and
581 /// `await` will be a no-op.581 /// `await` will be a no-op.
582 ///582 ///
583 /// When this function returns non-null, the implementation guarantees that
584 /// a unit of concurrency has been assigned to the returned task.
585 ///
583 /// Thread-safe.586 /// Thread-safe.
584 async: *const fn (587 async: *const fn (
585 /// Corresponds to `Io.userdata`.588 /// Corresponds to `Io.userdata`.
...@@ -1024,6 +1027,10 @@ pub const Group = struct {...@@ -1024,6 +1027,10 @@ pub const Group = struct {
1024 ///1027 ///
1025 /// `function` *may* be called immediately, before `async` returns.1028 /// `function` *may* be called immediately, before `async` returns.
1026 ///1029 ///
1030 /// When this function returns, it is guaranteed that `function` has
1031 /// already been called and completed, or it has successfully been assigned
1032 /// a unit of concurrency.
1033 ///
1027 /// After this is called, `wait` or `cancel` must be called before the1034 /// After this is called, `wait` or `cancel` must be called before the
1028 /// group is deinitialized.1035 /// group is deinitialized.
1029 ///1036 ///
...@@ -1094,6 +1101,10 @@ pub fn Select(comptime U: type) type {...@@ -1094,6 +1101,10 @@ pub fn Select(comptime U: type) type {
1094 ///1101 ///
1095 /// `function` *may* be called immediately, before `async` returns.1102 /// `function` *may* be called immediately, before `async` returns.
1096 ///1103 ///
1104 /// When this function returns, it is guaranteed that `function` has
1105 /// already been called and completed, or it has successfully been
1106 /// assigned a unit of concurrency.
1107 ///
1097 /// After this is called, `wait` or `cancel` must be called before the1108 /// After this is called, `wait` or `cancel` must be called before the
1098 /// select is deinitialized.1109 /// select is deinitialized.
1099 ///1110 ///
...@@ -1524,8 +1535,11 @@ pub fn Queue(Elem: type) type {...@@ -1524,8 +1535,11 @@ pub fn Queue(Elem: type) type {
1524/// not guaranteed to be available until `await` is called.1535/// not guaranteed to be available until `await` is called.
1525///1536///
1526/// `function` *may* be called immediately, before `async` returns. This has1537/// `function` *may* be called immediately, before `async` returns. This has
1527/// weaker guarantees than `concurrent`, making more portable and1538/// weaker guarantees than `concurrent`, making more portable and reusable.
1528/// reusable.1539///
1540/// When this function returns, it is guaranteed that `function` has already
1541/// been called and completed, or it has successfully been assigned a unit of
1542/// concurrency.
1529///1543///
1530/// See also:1544/// See also:
1531/// * `Group`1545/// * `Group`
lib/std/Io/Threaded.zig+110-108
...@@ -13,6 +13,7 @@ const net = std.Io.net;...@@ -13,6 +13,7 @@ const net = std.Io.net;
13const HostName = std.Io.net.HostName;13const HostName = std.Io.net.HostName;
14const IpAddress = std.Io.net.IpAddress;14const IpAddress = std.Io.net.IpAddress;
15const Allocator = std.mem.Allocator;15const Allocator = std.mem.Allocator;
16const Alignment = std.mem.Alignment;
16const assert = std.debug.assert;17const assert = std.debug.assert;
17const posix = std.posix;18const posix = std.posix;
1819
...@@ -22,10 +23,30 @@ mutex: std.Thread.Mutex = .{},...@@ -22,10 +23,30 @@ mutex: std.Thread.Mutex = .{},
22cond: std.Thread.Condition = .{},23cond: std.Thread.Condition = .{},
23run_queue: std.SinglyLinkedList = .{},24run_queue: std.SinglyLinkedList = .{},
24join_requested: bool = false,25join_requested: bool = false,
25threads: std.ArrayList(std.Thread),
26stack_size: usize,26stack_size: usize,
27cpu_count: std.Thread.CpuCountError!usize,27/// All threads are spawned detached; this is how we wait until they all exit.
28concurrent_count: usize,28wait_group: std.Thread.WaitGroup = .{},
29/// Maximum thread pool size (excluding main thread) when dispatching async
30/// tasks. Until this limit, calls to `Io.async` when all threads are busy will
31/// cause a new thread to be spawned and permanently added to the pool. After
32/// this limit, calls to `Io.async` when all threads are busy run the task
33/// immediately.
34///
35/// Defaults to a number equal to logical CPU cores.
36async_limit: Io.Limit,
37/// Maximum thread pool size (excluding main thread) for dispatching concurrent
38/// tasks. Until this limit, calls to `Io.concurrent` will increase the thread
39/// pool size.
40///
41/// concurrent tasks. After this number, calls to `Io.concurrent` return
42/// `error.ConcurrencyUnavailable`.
43concurrent_limit: Io.Limit = .unlimited,
44/// Error from calling `std.Thread.getCpuCount` in `init`.
45cpu_count_error: ?std.Thread.CpuCountError,
46/// Number of threads that are unavailable to take tasks. To calculate
47/// available count, subtract this from either `async_limit` or
48/// `concurrent_limit`.
49busy_count: usize = 0,
2950
30wsa: if (is_windows) Wsa else struct {} = .{},51wsa: if (is_windows) Wsa else struct {} = .{},
3152
...@@ -70,8 +91,6 @@ const Closure = struct {...@@ -70,8 +91,6 @@ const Closure = struct {
70 start: Start,91 start: Start,
71 node: std.SinglyLinkedList.Node = .{},92 node: std.SinglyLinkedList.Node = .{},
72 cancel_tid: CancelId,93 cancel_tid: CancelId,
73 /// Whether this task bumps minimum number of threads in the pool.
74 is_concurrent: bool,
7594
76 const Start = *const fn (*Closure) void;95 const Start = *const fn (*Closure) void;
7796
...@@ -90,8 +109,6 @@ const Closure = struct {...@@ -90,8 +109,6 @@ const Closure = struct {
90 }109 }
91};110};
92111
93pub const InitError = std.Thread.CpuCountError || Allocator.Error;
94
95/// Related:112/// Related:
96/// * `init_single_threaded`113/// * `init_single_threaded`
97pub fn init(114pub fn init(
...@@ -103,21 +120,20 @@ pub fn init(...@@ -103,21 +120,20 @@ pub fn init(
103 /// here.120 /// here.
104 gpa: Allocator,121 gpa: Allocator,
105) Threaded {122) Threaded {
123 if (builtin.single_threaded) return .init_single_threaded;
124
125 const cpu_count = std.Thread.getCpuCount();
126
106 var t: Threaded = .{127 var t: Threaded = .{
107 .allocator = gpa,128 .allocator = gpa,
108 .threads = .empty,
109 .stack_size = std.Thread.SpawnConfig.default_stack_size,129 .stack_size = std.Thread.SpawnConfig.default_stack_size,
110 .cpu_count = std.Thread.getCpuCount(),130 .async_limit = if (cpu_count) |n| .limited(n - 1) else |_| .nothing,
111 .concurrent_count = 0,131 .cpu_count_error = if (cpu_count) |_| null else |e| e,
112 .old_sig_io = undefined,132 .old_sig_io = undefined,
113 .old_sig_pipe = undefined,133 .old_sig_pipe = undefined,
114 .have_signal_handler = false,134 .have_signal_handler = false,
115 };135 };
116136
117 if (t.cpu_count) |n| {
118 t.threads.ensureTotalCapacityPrecise(gpa, n - 1) catch {};
119 } else |_| {}
120
121 if (posix.Sigaction != void) {137 if (posix.Sigaction != void) {
122 // This causes sending `posix.SIG.IO` to thread to interrupt blocking138 // This causes sending `posix.SIG.IO` to thread to interrupt blocking
123 // syscalls, returning `posix.E.INTR`.139 // syscalls, returning `posix.E.INTR`.
...@@ -142,19 +158,17 @@ pub fn init(...@@ -142,19 +158,17 @@ pub fn init(
142/// * `deinit` is safe, but unnecessary to call.158/// * `deinit` is safe, but unnecessary to call.
143pub const init_single_threaded: Threaded = .{159pub const init_single_threaded: Threaded = .{
144 .allocator = .failing,160 .allocator = .failing,
145 .threads = .empty,
146 .stack_size = std.Thread.SpawnConfig.default_stack_size,161 .stack_size = std.Thread.SpawnConfig.default_stack_size,
147 .cpu_count = 1,162 .async_limit = .nothing,
148 .concurrent_count = 0,163 .cpu_count_error = null,
164 .concurrent_limit = .nothing,
149 .old_sig_io = undefined,165 .old_sig_io = undefined,
150 .old_sig_pipe = undefined,166 .old_sig_pipe = undefined,
151 .have_signal_handler = false,167 .have_signal_handler = false,
152};168};
153169
154pub fn deinit(t: *Threaded) void {170pub fn deinit(t: *Threaded) void {
155 const gpa = t.allocator;
156 t.join();171 t.join();
157 t.threads.deinit(gpa);
158 if (is_windows and t.wsa.status == .initialized) {172 if (is_windows and t.wsa.status == .initialized) {
159 if (ws2_32.WSACleanup() != 0) recoverableOsBugDetected();173 if (ws2_32.WSACleanup() != 0) recoverableOsBugDetected();
160 }174 }
...@@ -173,10 +187,12 @@ fn join(t: *Threaded) void {...@@ -173,10 +187,12 @@ fn join(t: *Threaded) void {
173 t.join_requested = true;187 t.join_requested = true;
174 }188 }
175 t.cond.broadcast();189 t.cond.broadcast();
176 for (t.threads.items) |thread| thread.join();190 t.wait_group.wait();
177}191}
178192
179fn worker(t: *Threaded) void {193fn worker(t: *Threaded) void {
194 defer t.wait_group.finish();
195
180 t.mutex.lock();196 t.mutex.lock();
181 defer t.mutex.unlock();197 defer t.mutex.unlock();
182198
...@@ -184,12 +200,9 @@ fn worker(t: *Threaded) void {...@@ -184,12 +200,9 @@ fn worker(t: *Threaded) void {
184 while (t.run_queue.popFirst()) |closure_node| {200 while (t.run_queue.popFirst()) |closure_node| {
185 t.mutex.unlock();201 t.mutex.unlock();
186 const closure: *Closure = @fieldParentPtr("node", closure_node);202 const closure: *Closure = @fieldParentPtr("node", closure_node);
187 const is_concurrent = closure.is_concurrent;
188 closure.start(closure);203 closure.start(closure);
189 t.mutex.lock();204 t.mutex.lock();
190 if (is_concurrent) {205 t.busy_count -= 1;
191 t.concurrent_count -= 1;
192 }
193 }206 }
194 if (t.join_requested) break;207 if (t.join_requested) break;
195 t.cond.wait(&t.mutex);208 t.cond.wait(&t.mutex);
...@@ -387,7 +400,7 @@ const AsyncClosure = struct {...@@ -387,7 +400,7 @@ const AsyncClosure = struct {
387 func: *const fn (context: *anyopaque, result: *anyopaque) void,400 func: *const fn (context: *anyopaque, result: *anyopaque) void,
388 reset_event: ResetEvent,401 reset_event: ResetEvent,
389 select_condition: ?*ResetEvent,402 select_condition: ?*ResetEvent,
390 context_alignment: std.mem.Alignment,403 context_alignment: Alignment,
391 result_offset: usize,404 result_offset: usize,
392 alloc_len: usize,405 alloc_len: usize,
393406
...@@ -432,11 +445,10 @@ const AsyncClosure = struct {...@@ -432,11 +445,10 @@ const AsyncClosure = struct {
432445
433 fn init(446 fn init(
434 gpa: Allocator,447 gpa: Allocator,
435 mode: enum { async, concurrent },
436 result_len: usize,448 result_len: usize,
437 result_alignment: std.mem.Alignment,449 result_alignment: Alignment,
438 context: []const u8,450 context: []const u8,
439 context_alignment: std.mem.Alignment,451 context_alignment: Alignment,
440 func: *const fn (context: *const anyopaque, result: *anyopaque) void,452 func: *const fn (context: *const anyopaque, result: *anyopaque) void,
441 ) Allocator.Error!*AsyncClosure {453 ) Allocator.Error!*AsyncClosure {
442 const max_context_misalignment = context_alignment.toByteUnits() -| @alignOf(AsyncClosure);454 const max_context_misalignment = context_alignment.toByteUnits() -| @alignOf(AsyncClosure);
...@@ -454,10 +466,6 @@ const AsyncClosure = struct {...@@ -454,10 +466,6 @@ const AsyncClosure = struct {
454 .closure = .{466 .closure = .{
455 .cancel_tid = .none,467 .cancel_tid = .none,
456 .start = start,468 .start = start,
457 .is_concurrent = switch (mode) {
458 .async => false,
459 .concurrent => true,
460 },
461 },469 },
462 .func = func,470 .func = func,
463 .context_alignment = context_alignment,471 .context_alignment = context_alignment,
...@@ -470,10 +478,15 @@ const AsyncClosure = struct {...@@ -470,10 +478,15 @@ const AsyncClosure = struct {
470 return ac;478 return ac;
471 }479 }
472480
473 fn waitAndDeinit(ac: *AsyncClosure, gpa: Allocator, result: []u8) void {481 fn waitAndDeinit(ac: *AsyncClosure, t: *Threaded, result: []u8) void {
474 ac.reset_event.waitUncancelable();482 ac.reset_event.wait(t) catch |err| switch (err) {
483 error.Canceled => {
484 ac.closure.requestCancel();
485 ac.reset_event.waitUncancelable();
486 },
487 };
475 @memcpy(result, ac.resultPointer()[0..result.len]);488 @memcpy(result, ac.resultPointer()[0..result.len]);
476 ac.deinit(gpa);489 ac.deinit(t.allocator);
477 }490 }
478491
479 fn deinit(ac: *AsyncClosure, gpa: Allocator) void {492 fn deinit(ac: *AsyncClosure, gpa: Allocator) void {
...@@ -485,60 +498,50 @@ const AsyncClosure = struct {...@@ -485,60 +498,50 @@ const AsyncClosure = struct {
485fn async(498fn async(
486 userdata: ?*anyopaque,499 userdata: ?*anyopaque,
487 result: []u8,500 result: []u8,
488 result_alignment: std.mem.Alignment,501 result_alignment: Alignment,
489 context: []const u8,502 context: []const u8,
490 context_alignment: std.mem.Alignment,503 context_alignment: Alignment,
491 start: *const fn (context: *const anyopaque, result: *anyopaque) void,504 start: *const fn (context: *const anyopaque, result: *anyopaque) void,
492) ?*Io.AnyFuture {505) ?*Io.AnyFuture {
493 if (builtin.single_threaded) {506 const t: *Threaded = @ptrCast(@alignCast(userdata));
507 if (builtin.single_threaded or t.async_limit == .nothing) {
494 start(context.ptr, result.ptr);508 start(context.ptr, result.ptr);
495 return null;509 return null;
496 }510 }
497
498 const t: *Threaded = @ptrCast(@alignCast(userdata));
499 const cpu_count = t.cpu_count catch {
500 return concurrent(userdata, result.len, result_alignment, context, context_alignment, start) catch {
501 start(context.ptr, result.ptr);
502 return null;
503 };
504 };
505
506 const gpa = t.allocator;511 const gpa = t.allocator;
507 const ac = AsyncClosure.init(gpa, .async, result.len, result_alignment, context, context_alignment, start) catch {512 const ac = AsyncClosure.init(gpa, result.len, result_alignment, context, context_alignment, start) catch {
508 start(context.ptr, result.ptr);513 start(context.ptr, result.ptr);
509 return null;514 return null;
510 };515 };
511516
512 t.mutex.lock();517 t.mutex.lock();
513518
514 const thread_capacity = cpu_count - 1 + t.concurrent_count;519 const busy_count = t.busy_count;
515520
516 t.threads.ensureTotalCapacityPrecise(gpa, thread_capacity) catch {521 if (busy_count >= @intFromEnum(t.async_limit)) {
517 t.mutex.unlock();522 t.mutex.unlock();
518 ac.deinit(gpa);523 ac.deinit(gpa);
519 start(context.ptr, result.ptr);524 start(context.ptr, result.ptr);
520 return null;525 return null;
521 };526 }
522527
523 t.run_queue.prepend(&ac.closure.node);528 t.busy_count = busy_count + 1;
524529
525 if (t.threads.items.len < thread_capacity) {530 const pool_size = t.wait_group.value();
531 if (pool_size - busy_count == 0) {
532 t.wait_group.start();
526 const thread = std.Thread.spawn(.{ .stack_size = t.stack_size }, worker, .{t}) catch {533 const thread = std.Thread.spawn(.{ .stack_size = t.stack_size }, worker, .{t}) catch {
527 if (t.threads.items.len == 0) {534 t.wait_group.finish();
528 assert(t.run_queue.popFirst() == &ac.closure.node);535 t.busy_count = busy_count;
529 t.mutex.unlock();
530 ac.deinit(gpa);
531 start(context.ptr, result.ptr);
532 return null;
533 }
534 // Rely on other workers to do it.
535 t.mutex.unlock();536 t.mutex.unlock();
536 t.cond.signal();537 ac.deinit(gpa);
537 return @ptrCast(ac);538 start(context.ptr, result.ptr);
539 return null;
538 };540 };
539 t.threads.appendAssumeCapacity(thread);541 thread.detach();
540 }542 }
541543
544 t.run_queue.prepend(&ac.closure.node);
542 t.mutex.unlock();545 t.mutex.unlock();
543 t.cond.signal();546 t.cond.signal();
544 return @ptrCast(ac);547 return @ptrCast(ac);
...@@ -547,45 +550,42 @@ fn async(...@@ -547,45 +550,42 @@ fn async(
547fn concurrent(550fn concurrent(
548 userdata: ?*anyopaque,551 userdata: ?*anyopaque,
549 result_len: usize,552 result_len: usize,
550 result_alignment: std.mem.Alignment,553 result_alignment: Alignment,
551 context: []const u8,554 context: []const u8,
552 context_alignment: std.mem.Alignment,555 context_alignment: Alignment,
553 start: *const fn (context: *const anyopaque, result: *anyopaque) void,556 start: *const fn (context: *const anyopaque, result: *anyopaque) void,
554) Io.ConcurrentError!*Io.AnyFuture {557) Io.ConcurrentError!*Io.AnyFuture {
555 if (builtin.single_threaded) return error.ConcurrencyUnavailable;558 if (builtin.single_threaded) return error.ConcurrencyUnavailable;
556559
557 const t: *Threaded = @ptrCast(@alignCast(userdata));560 const t: *Threaded = @ptrCast(@alignCast(userdata));
558 const cpu_count = t.cpu_count catch 1;
559561
560 const gpa = t.allocator;562 const gpa = t.allocator;
561 const ac = AsyncClosure.init(gpa, .concurrent, result_len, result_alignment, context, context_alignment, start) catch {563 const ac = AsyncClosure.init(gpa, result_len, result_alignment, context, context_alignment, start) catch
562 return error.ConcurrencyUnavailable;564 return error.ConcurrencyUnavailable;
563 };565 errdefer ac.deinit(gpa);
564566
565 t.mutex.lock();567 t.mutex.lock();
568 defer t.mutex.unlock();
566569
567 t.concurrent_count += 1;570 const busy_count = t.busy_count;
568 const thread_capacity = cpu_count - 1 + t.concurrent_count;
569571
570 t.threads.ensureTotalCapacity(gpa, thread_capacity) catch {572 if (busy_count >= @intFromEnum(t.concurrent_limit))
571 t.mutex.unlock();
572 ac.deinit(gpa);
573 return error.ConcurrencyUnavailable;573 return error.ConcurrencyUnavailable;
574 };
575574
576 t.run_queue.prepend(&ac.closure.node);575 t.busy_count = busy_count + 1;
576 errdefer t.busy_count = busy_count;
577577
578 if (t.threads.items.len < thread_capacity) {578 const pool_size = t.wait_group.value();
579 const thread = std.Thread.spawn(.{ .stack_size = t.stack_size }, worker, .{t}) catch {579 if (pool_size - busy_count == 0) {
580 assert(t.run_queue.popFirst() == &ac.closure.node);580 t.wait_group.start();
581 t.mutex.unlock();581 errdefer t.wait_group.finish();
582 ac.deinit(gpa);582
583 const thread = std.Thread.spawn(.{ .stack_size = t.stack_size }, worker, .{t}) catch
583 return error.ConcurrencyUnavailable;584 return error.ConcurrencyUnavailable;
584 };585 thread.detach();
585 t.threads.appendAssumeCapacity(thread);
586 }586 }
587587
588 t.mutex.unlock();588 t.run_queue.prepend(&ac.closure.node);
589 t.cond.signal();589 t.cond.signal();
590 return @ptrCast(ac);590 return @ptrCast(ac);
591}591}
...@@ -597,7 +597,7 @@ const GroupClosure = struct {...@@ -597,7 +597,7 @@ const GroupClosure = struct {
597 /// Points to sibling `GroupClosure`. Used for walking the group to cancel all.597 /// Points to sibling `GroupClosure`. Used for walking the group to cancel all.
598 node: std.SinglyLinkedList.Node,598 node: std.SinglyLinkedList.Node,
599 func: *const fn (*Io.Group, context: *anyopaque) void,599 func: *const fn (*Io.Group, context: *anyopaque) void,
600 context_alignment: std.mem.Alignment,600 context_alignment: Alignment,
601 alloc_len: usize,601 alloc_len: usize,
602602
603 fn start(closure: *Closure) void {603 fn start(closure: *Closure) void {
...@@ -638,7 +638,7 @@ const GroupClosure = struct {...@@ -638,7 +638,7 @@ const GroupClosure = struct {
638 t: *Threaded,638 t: *Threaded,
639 group: *Io.Group,639 group: *Io.Group,
640 context: []const u8,640 context: []const u8,
641 context_alignment: std.mem.Alignment,641 context_alignment: Alignment,
642 func: *const fn (*Io.Group, context: *const anyopaque) void,642 func: *const fn (*Io.Group, context: *const anyopaque) void,
643 ) Allocator.Error!*GroupClosure {643 ) Allocator.Error!*GroupClosure {
644 const max_context_misalignment = context_alignment.toByteUnits() -| @alignOf(GroupClosure);644 const max_context_misalignment = context_alignment.toByteUnits() -| @alignOf(GroupClosure);
...@@ -652,7 +652,6 @@ const GroupClosure = struct {...@@ -652,7 +652,6 @@ const GroupClosure = struct {
652 .closure = .{652 .closure = .{
653 .cancel_tid = .none,653 .cancel_tid = .none,
654 .start = start,654 .start = start,
655 .is_concurrent = false,
656 },655 },
657 .t = t,656 .t = t,
658 .group = group,657 .group = group,
...@@ -678,45 +677,48 @@ fn groupAsync(...@@ -678,45 +677,48 @@ fn groupAsync(
678 userdata: ?*anyopaque,677 userdata: ?*anyopaque,
679 group: *Io.Group,678 group: *Io.Group,
680 context: []const u8,679 context: []const u8,
681 context_alignment: std.mem.Alignment,680 context_alignment: Alignment,
682 start: *const fn (*Io.Group, context: *const anyopaque) void,681 start: *const fn (*Io.Group, context: *const anyopaque) void,
683) void {682) void {
684 if (builtin.single_threaded) return start(group, context.ptr);
685
686 const t: *Threaded = @ptrCast(@alignCast(userdata));683 const t: *Threaded = @ptrCast(@alignCast(userdata));
687 const cpu_count = t.cpu_count catch 1;684 if (builtin.single_threaded or t.async_limit == .nothing)
685 return start(group, context.ptr);
688686
689 const gpa = t.allocator;687 const gpa = t.allocator;
690 const gc = GroupClosure.init(gpa, t, group, context, context_alignment, start) catch {688 const gc = GroupClosure.init(gpa, t, group, context, context_alignment, start) catch
691 return start(group, context.ptr);689 return start(group, context.ptr);
692 };
693690
694 t.mutex.lock();691 t.mutex.lock();
695692
696 // Append to the group linked list inside the mutex to make `Io.Group.async` thread-safe.693 const busy_count = t.busy_count;
697 gc.node = .{ .next = @ptrCast(@alignCast(group.token)) };
698 group.token = &gc.node;
699694
700 const thread_capacity = cpu_count - 1 + t.concurrent_count;695 if (busy_count >= @intFromEnum(t.async_limit)) {
701
702 t.threads.ensureTotalCapacityPrecise(gpa, thread_capacity) catch {
703 t.mutex.unlock();696 t.mutex.unlock();
704 gc.deinit(gpa);697 gc.deinit(gpa);
705 return start(group, context.ptr);698 return start(group, context.ptr);
706 };699 }
707700
708 t.run_queue.prepend(&gc.closure.node);701 t.busy_count = busy_count + 1;
709702
710 if (t.threads.items.len < thread_capacity) {703 const pool_size = t.wait_group.value();
704 if (pool_size - busy_count == 0) {
705 t.wait_group.start();
711 const thread = std.Thread.spawn(.{ .stack_size = t.stack_size }, worker, .{t}) catch {706 const thread = std.Thread.spawn(.{ .stack_size = t.stack_size }, worker, .{t}) catch {
712 assert(t.run_queue.popFirst() == &gc.closure.node);707 t.wait_group.finish();
708 t.busy_count = busy_count;
713 t.mutex.unlock();709 t.mutex.unlock();
714 gc.deinit(gpa);710 gc.deinit(gpa);
715 return start(group, context.ptr);711 return start(group, context.ptr);
716 };712 };
717 t.threads.appendAssumeCapacity(thread);713 thread.detach();
718 }714 }
719715
716 // Append to the group linked list inside the mutex to make `Io.Group.async` thread-safe.
717 gc.node = .{ .next = @ptrCast(@alignCast(group.token)) };
718 group.token = &gc.node;
719
720 t.run_queue.prepend(&gc.closure.node);
721
720 // This needs to be done before unlocking the mutex to avoid a race with722 // This needs to be done before unlocking the mutex to avoid a race with
721 // the associated task finishing.723 // the associated task finishing.
722 const group_state: *std.atomic.Value(usize) = @ptrCast(&group.state);724 const group_state: *std.atomic.Value(usize) = @ptrCast(&group.state);
...@@ -794,25 +796,25 @@ fn await(...@@ -794,25 +796,25 @@ fn await(
794 userdata: ?*anyopaque,796 userdata: ?*anyopaque,
795 any_future: *Io.AnyFuture,797 any_future: *Io.AnyFuture,
796 result: []u8,798 result: []u8,
797 result_alignment: std.mem.Alignment,799 result_alignment: Alignment,
798) void {800) void {
799 _ = result_alignment;801 _ = result_alignment;
800 const t: *Threaded = @ptrCast(@alignCast(userdata));802 const t: *Threaded = @ptrCast(@alignCast(userdata));
801 const closure: *AsyncClosure = @ptrCast(@alignCast(any_future));803 const closure: *AsyncClosure = @ptrCast(@alignCast(any_future));
802 closure.waitAndDeinit(t.allocator, result);804 closure.waitAndDeinit(t, result);
803}805}
804806
805fn cancel(807fn cancel(
806 userdata: ?*anyopaque,808 userdata: ?*anyopaque,
807 any_future: *Io.AnyFuture,809 any_future: *Io.AnyFuture,
808 result: []u8,810 result: []u8,
809 result_alignment: std.mem.Alignment,811 result_alignment: Alignment,
810) void {812) void {
811 _ = result_alignment;813 _ = result_alignment;
812 const t: *Threaded = @ptrCast(@alignCast(userdata));814 const t: *Threaded = @ptrCast(@alignCast(userdata));
813 const ac: *AsyncClosure = @ptrCast(@alignCast(any_future));815 const ac: *AsyncClosure = @ptrCast(@alignCast(any_future));
814 ac.closure.requestCancel();816 ac.closure.requestCancel();
815 ac.waitAndDeinit(t.allocator, result);817 ac.waitAndDeinit(t, result);
816}818}
817819
818fn cancelRequested(userdata: ?*anyopaque) bool {820fn cancelRequested(userdata: ?*anyopaque) bool {
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.zig+7-6
...@@ -1,13 +1,14 @@...@@ -1,13 +1,14 @@
1//! This struct represents a kernel thread, and acts as a namespace for concurrency1//! This struct represents a kernel thread, and acts as a namespace for
2//! primitives that operate on kernel threads. For concurrency primitives that support2//! concurrency primitives that operate on kernel threads. For concurrency
3//! both evented I/O and async I/O, see the respective names in the top level std namespace.3//! primitives that interact with the I/O interface, see `std.Io`.
44
5const std = @import("std.zig");
6const builtin = @import("builtin");5const builtin = @import("builtin");
7const math = std.math;
8const assert = std.debug.assert;
9const target = builtin.target;6const target = builtin.target;
10const native_os = builtin.os.tag;7const native_os = builtin.os.tag;
8
9const std = @import("std.zig");
10const math = std.math;
11const assert = std.debug.assert;
11const posix = std.posix;12const posix = std.posix;
12const windows = std.os.windows;13const windows = std.os.windows;
13const testing = std.testing;14const testing = std.testing;
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(