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 {
580580 /// If it returns `null` it means `result` has been already populated and
581581 /// `await` will be a no-op.
582582 ///
583 /// When this function returns non-null, the implementation guarantees that
584 /// a unit of concurrency has been assigned to the returned task.
585 ///
583586 /// Thread-safe.
584587 async: *const fn (
585588 /// Corresponds to `Io.userdata`.
......@@ -1024,6 +1027,10 @@ pub const Group = struct {
10241027 ///
10251028 /// `function` *may* be called immediately, before `async` returns.
10261029 ///
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 ///
10271034 /// After this is called, `wait` or `cancel` must be called before the
10281035 /// group is deinitialized.
10291036 ///
......@@ -1094,6 +1101,10 @@ pub fn Select(comptime U: type) type {
10941101 ///
10951102 /// `function` *may* be called immediately, before `async` returns.
10961103 ///
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 ///
10971108 /// After this is called, `wait` or `cancel` must be called before the
10981109 /// select is deinitialized.
10991110 ///
......@@ -1524,8 +1535,11 @@ pub fn Queue(Elem: type) type {
15241535/// not guaranteed to be available until `await` is called.
15251536///
15261537/// `function` *may* be called immediately, before `async` returns. This has
1527/// weaker guarantees than `concurrent`, making more portable and
1528/// reusable.
1538/// weaker guarantees than `concurrent`, making more portable and 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.
15291543///
15301544/// See also:
15311545/// * `Group`
lib/std/Io/Threaded.zig+110-108
......@@ -13,6 +13,7 @@ const net = std.Io.net;
1313const HostName = std.Io.net.HostName;
1414const IpAddress = std.Io.net.IpAddress;
1515const Allocator = std.mem.Allocator;
16const Alignment = std.mem.Alignment;
1617const assert = std.debug.assert;
1718const posix = std.posix;
1819
......@@ -22,10 +23,30 @@ mutex: std.Thread.Mutex = .{},
2223cond: std.Thread.Condition = .{},
2324run_queue: std.SinglyLinkedList = .{},
2425join_requested: bool = false,
25threads: std.ArrayList(std.Thread),
2626stack_size: usize,
27cpu_count: std.Thread.CpuCountError!usize,
28concurrent_count: usize,
27/// All threads are spawned detached; this is how we wait until they all exit.
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
3051wsa: if (is_windows) Wsa else struct {} = .{},
3152
......@@ -70,8 +91,6 @@ const Closure = struct {
7091 start: Start,
7192 node: std.SinglyLinkedList.Node = .{},
7293 cancel_tid: CancelId,
73 /// Whether this task bumps minimum number of threads in the pool.
74 is_concurrent: bool,
7594
7695 const Start = *const fn (*Closure) void;
7796
......@@ -90,8 +109,6 @@ const Closure = struct {
90109 }
91110};
92111
93pub const InitError = std.Thread.CpuCountError || Allocator.Error;
94
95112/// Related:
96113/// * `init_single_threaded`
97114pub fn init(
......@@ -103,21 +120,20 @@ pub fn init(
103120 /// here.
104121 gpa: Allocator,
105122) Threaded {
123 if (builtin.single_threaded) return .init_single_threaded;
124
125 const cpu_count = std.Thread.getCpuCount();
126
106127 var t: Threaded = .{
107128 .allocator = gpa,
108 .threads = .empty,
109129 .stack_size = std.Thread.SpawnConfig.default_stack_size,
110 .cpu_count = std.Thread.getCpuCount(),
111 .concurrent_count = 0,
130 .async_limit = if (cpu_count) |n| .limited(n - 1) else |_| .nothing,
131 .cpu_count_error = if (cpu_count) |_| null else |e| e,
112132 .old_sig_io = undefined,
113133 .old_sig_pipe = undefined,
114134 .have_signal_handler = false,
115135 };
116136
117 if (t.cpu_count) |n| {
118 t.threads.ensureTotalCapacityPrecise(gpa, n - 1) catch {};
119 } else |_| {}
120
121137 if (posix.Sigaction != void) {
122138 // This causes sending `posix.SIG.IO` to thread to interrupt blocking
123139 // syscalls, returning `posix.E.INTR`.
......@@ -142,19 +158,17 @@ pub fn init(
142158/// * `deinit` is safe, but unnecessary to call.
143159pub const init_single_threaded: Threaded = .{
144160 .allocator = .failing,
145 .threads = .empty,
146161 .stack_size = std.Thread.SpawnConfig.default_stack_size,
147 .cpu_count = 1,
148 .concurrent_count = 0,
162 .async_limit = .nothing,
163 .cpu_count_error = null,
164 .concurrent_limit = .nothing,
149165 .old_sig_io = undefined,
150166 .old_sig_pipe = undefined,
151167 .have_signal_handler = false,
152168};
153169
154170pub fn deinit(t: *Threaded) void {
155 const gpa = t.allocator;
156171 t.join();
157 t.threads.deinit(gpa);
158172 if (is_windows and t.wsa.status == .initialized) {
159173 if (ws2_32.WSACleanup() != 0) recoverableOsBugDetected();
160174 }
......@@ -173,10 +187,12 @@ fn join(t: *Threaded) void {
173187 t.join_requested = true;
174188 }
175189 t.cond.broadcast();
176 for (t.threads.items) |thread| thread.join();
190 t.wait_group.wait();
177191}
178192
179193fn worker(t: *Threaded) void {
194 defer t.wait_group.finish();
195
180196 t.mutex.lock();
181197 defer t.mutex.unlock();
182198
......@@ -184,12 +200,9 @@ fn worker(t: *Threaded) void {
184200 while (t.run_queue.popFirst()) |closure_node| {
185201 t.mutex.unlock();
186202 const closure: *Closure = @fieldParentPtr("node", closure_node);
187 const is_concurrent = closure.is_concurrent;
188203 closure.start(closure);
189204 t.mutex.lock();
190 if (is_concurrent) {
191 t.concurrent_count -= 1;
192 }
205 t.busy_count -= 1;
193206 }
194207 if (t.join_requested) break;
195208 t.cond.wait(&t.mutex);
......@@ -387,7 +400,7 @@ const AsyncClosure = struct {
387400 func: *const fn (context: *anyopaque, result: *anyopaque) void,
388401 reset_event: ResetEvent,
389402 select_condition: ?*ResetEvent,
390 context_alignment: std.mem.Alignment,
403 context_alignment: Alignment,
391404 result_offset: usize,
392405 alloc_len: usize,
393406
......@@ -432,11 +445,10 @@ const AsyncClosure = struct {
432445
433446 fn init(
434447 gpa: Allocator,
435 mode: enum { async, concurrent },
436448 result_len: usize,
437 result_alignment: std.mem.Alignment,
449 result_alignment: Alignment,
438450 context: []const u8,
439 context_alignment: std.mem.Alignment,
451 context_alignment: Alignment,
440452 func: *const fn (context: *const anyopaque, result: *anyopaque) void,
441453 ) Allocator.Error!*AsyncClosure {
442454 const max_context_misalignment = context_alignment.toByteUnits() -| @alignOf(AsyncClosure);
......@@ -454,10 +466,6 @@ const AsyncClosure = struct {
454466 .closure = .{
455467 .cancel_tid = .none,
456468 .start = start,
457 .is_concurrent = switch (mode) {
458 .async => false,
459 .concurrent => true,
460 },
461469 },
462470 .func = func,
463471 .context_alignment = context_alignment,
......@@ -470,10 +478,15 @@ const AsyncClosure = struct {
470478 return ac;
471479 }
472480
473 fn waitAndDeinit(ac: *AsyncClosure, gpa: Allocator, result: []u8) void {
474 ac.reset_event.waitUncancelable();
481 fn waitAndDeinit(ac: *AsyncClosure, t: *Threaded, result: []u8) void {
482 ac.reset_event.wait(t) catch |err| switch (err) {
483 error.Canceled => {
484 ac.closure.requestCancel();
485 ac.reset_event.waitUncancelable();
486 },
487 };
475488 @memcpy(result, ac.resultPointer()[0..result.len]);
476 ac.deinit(gpa);
489 ac.deinit(t.allocator);
477490 }
478491
479492 fn deinit(ac: *AsyncClosure, gpa: Allocator) void {
......@@ -485,60 +498,50 @@ const AsyncClosure = struct {
485498fn async(
486499 userdata: ?*anyopaque,
487500 result: []u8,
488 result_alignment: std.mem.Alignment,
501 result_alignment: Alignment,
489502 context: []const u8,
490 context_alignment: std.mem.Alignment,
503 context_alignment: Alignment,
491504 start: *const fn (context: *const anyopaque, result: *anyopaque) void,
492505) ?*Io.AnyFuture {
493 if (builtin.single_threaded) {
506 const t: *Threaded = @ptrCast(@alignCast(userdata));
507 if (builtin.single_threaded or t.async_limit == .nothing) {
494508 start(context.ptr, result.ptr);
495509 return null;
496510 }
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
506511 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 {
508513 start(context.ptr, result.ptr);
509514 return null;
510515 };
511516
512517 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)) {
517522 t.mutex.unlock();
518523 ac.deinit(gpa);
519524 start(context.ptr, result.ptr);
520525 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();
526533 const thread = std.Thread.spawn(.{ .stack_size = t.stack_size }, worker, .{t}) catch {
527 if (t.threads.items.len == 0) {
528 assert(t.run_queue.popFirst() == &ac.closure.node);
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.
534 t.wait_group.finish();
535 t.busy_count = busy_count;
535536 t.mutex.unlock();
536 t.cond.signal();
537 return @ptrCast(ac);
537 ac.deinit(gpa);
538 start(context.ptr, result.ptr);
539 return null;
538540 };
539 t.threads.appendAssumeCapacity(thread);
541 thread.detach();
540542 }
541543
544 t.run_queue.prepend(&ac.closure.node);
542545 t.mutex.unlock();
543546 t.cond.signal();
544547 return @ptrCast(ac);
......@@ -547,45 +550,42 @@ fn async(
547550fn concurrent(
548551 userdata: ?*anyopaque,
549552 result_len: usize,
550 result_alignment: std.mem.Alignment,
553 result_alignment: Alignment,
551554 context: []const u8,
552 context_alignment: std.mem.Alignment,
555 context_alignment: Alignment,
553556 start: *const fn (context: *const anyopaque, result: *anyopaque) void,
554557) Io.ConcurrentError!*Io.AnyFuture {
555558 if (builtin.single_threaded) return error.ConcurrencyUnavailable;
556559
557560 const t: *Threaded = @ptrCast(@alignCast(userdata));
558 const cpu_count = t.cpu_count catch 1;
559561
560562 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
562564 return error.ConcurrencyUnavailable;
563 };
565 errdefer ac.deinit(gpa);
564566
565567 t.mutex.lock();
568 defer t.mutex.unlock();
566569
567 t.concurrent_count += 1;
568 const thread_capacity = cpu_count - 1 + t.concurrent_count;
570 const busy_count = t.busy_count;
569571
570 t.threads.ensureTotalCapacity(gpa, thread_capacity) catch {
571 t.mutex.unlock();
572 ac.deinit(gpa);
572 if (busy_count >= @intFromEnum(t.concurrent_limit))
573573 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) {
579 const thread = std.Thread.spawn(.{ .stack_size = t.stack_size }, worker, .{t}) catch {
580 assert(t.run_queue.popFirst() == &ac.closure.node);
581 t.mutex.unlock();
582 ac.deinit(gpa);
578 const pool_size = t.wait_group.value();
579 if (pool_size - busy_count == 0) {
580 t.wait_group.start();
581 errdefer t.wait_group.finish();
582
583 const thread = std.Thread.spawn(.{ .stack_size = t.stack_size }, worker, .{t}) catch
583584 return error.ConcurrencyUnavailable;
584 };
585 t.threads.appendAssumeCapacity(thread);
585 thread.detach();
586586 }
587587
588 t.mutex.unlock();
588 t.run_queue.prepend(&ac.closure.node);
589589 t.cond.signal();
590590 return @ptrCast(ac);
591591}
......@@ -597,7 +597,7 @@ const GroupClosure = struct {
597597 /// Points to sibling `GroupClosure`. Used for walking the group to cancel all.
598598 node: std.SinglyLinkedList.Node,
599599 func: *const fn (*Io.Group, context: *anyopaque) void,
600 context_alignment: std.mem.Alignment,
600 context_alignment: Alignment,
601601 alloc_len: usize,
602602
603603 fn start(closure: *Closure) void {
......@@ -638,7 +638,7 @@ const GroupClosure = struct {
638638 t: *Threaded,
639639 group: *Io.Group,
640640 context: []const u8,
641 context_alignment: std.mem.Alignment,
641 context_alignment: Alignment,
642642 func: *const fn (*Io.Group, context: *const anyopaque) void,
643643 ) Allocator.Error!*GroupClosure {
644644 const max_context_misalignment = context_alignment.toByteUnits() -| @alignOf(GroupClosure);
......@@ -652,7 +652,6 @@ const GroupClosure = struct {
652652 .closure = .{
653653 .cancel_tid = .none,
654654 .start = start,
655 .is_concurrent = false,
656655 },
657656 .t = t,
658657 .group = group,
......@@ -678,45 +677,48 @@ fn groupAsync(
678677 userdata: ?*anyopaque,
679678 group: *Io.Group,
680679 context: []const u8,
681 context_alignment: std.mem.Alignment,
680 context_alignment: Alignment,
682681 start: *const fn (*Io.Group, context: *const anyopaque) void,
683682) void {
684 if (builtin.single_threaded) return start(group, context.ptr);
685
686683 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
689687 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
691689 return start(group, context.ptr);
692 };
693690
694691 t.mutex.lock();
695692
696 // Append to the group linked list inside the mutex to make `Io.Group.async` thread-safe.
697 gc.node = .{ .next = @ptrCast(@alignCast(group.token)) };
698 group.token = &gc.node;
693 const busy_count = t.busy_count;
699694
700 const thread_capacity = cpu_count - 1 + t.concurrent_count;
701
702 t.threads.ensureTotalCapacityPrecise(gpa, thread_capacity) catch {
695 if (busy_count >= @intFromEnum(t.async_limit)) {
703696 t.mutex.unlock();
704697 gc.deinit(gpa);
705698 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();
711706 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;
713709 t.mutex.unlock();
714710 gc.deinit(gpa);
715711 return start(group, context.ptr);
716712 };
717 t.threads.appendAssumeCapacity(thread);
713 thread.detach();
718714 }
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
720722 // This needs to be done before unlocking the mutex to avoid a race with
721723 // the associated task finishing.
722724 const group_state: *std.atomic.Value(usize) = @ptrCast(&group.state);
......@@ -794,25 +796,25 @@ fn await(
794796 userdata: ?*anyopaque,
795797 any_future: *Io.AnyFuture,
796798 result: []u8,
797 result_alignment: std.mem.Alignment,
799 result_alignment: Alignment,
798800) void {
799801 _ = result_alignment;
800802 const t: *Threaded = @ptrCast(@alignCast(userdata));
801803 const closure: *AsyncClosure = @ptrCast(@alignCast(any_future));
802 closure.waitAndDeinit(t.allocator, result);
804 closure.waitAndDeinit(t, result);
803805}
804806
805807fn cancel(
806808 userdata: ?*anyopaque,
807809 any_future: *Io.AnyFuture,
808810 result: []u8,
809 result_alignment: std.mem.Alignment,
811 result_alignment: Alignment,
810812) void {
811813 _ = result_alignment;
812814 const t: *Threaded = @ptrCast(@alignCast(userdata));
813815 const ac: *AsyncClosure = @ptrCast(@alignCast(any_future));
814816 ac.closure.requestCancel();
815 ac.waitAndDeinit(t.allocator, result);
817 ac.waitAndDeinit(t, result);
816818}
817819
818820fn cancelRequested(userdata: ?*anyopaque) bool {
lib/std/Io/Threaded/test.zig+2-2
......@@ -10,7 +10,7 @@ test "concurrent vs main prevents deadlock via oversubscription" {
1010 defer threaded.deinit();
1111 const io = threaded.io();
1212
13 threaded.cpu_count = 1;
13 threaded.async_limit = .nothing;
1414
1515 var queue: Io.Queue(u8) = .init(&.{});
1616
......@@ -38,7 +38,7 @@ test "concurrent vs concurrent prevents deadlock via oversubscription" {
3838 defer threaded.deinit();
3939 const io = threaded.io();
4040
41 threaded.cpu_count = 1;
41 threaded.async_limit = .nothing;
4242
4343 var queue: Io.Queue(u8) = .init(&.{});
4444
lib/std/Thread.zig+7-6
......@@ -1,13 +1,14 @@
1//! This struct represents a kernel thread, and acts as a namespace for concurrency
2//! primitives that operate on kernel threads. For concurrency primitives that support
3//! both evented I/O and async I/O, see the respective names in the top level std namespace.
1//! This struct represents a kernel thread, and acts as a namespace for
2//! concurrency primitives that operate on kernel threads. For concurrency
3//! primitives that interact with the I/O interface, see `std.Io`.
44
5const std = @import("std.zig");
65const builtin = @import("builtin");
7const math = std.math;
8const assert = std.debug.assert;
96const target = builtin.target;
107const native_os = builtin.os.tag;
8
9const std = @import("std.zig");
10const math = std.math;
11const assert = std.debug.assert;
1112const posix = std.posix;
1213const windows = std.os.windows;
1314const testing = std.testing;
lib/std/Thread/WaitGroup.zig+4
......@@ -60,6 +60,10 @@ pub fn isDone(wg: *WaitGroup) bool {
6060 return (state / one_pending) == 0;
6161}
6262
63pub fn value(wg: *WaitGroup) usize {
64 return wg.state.load(.monotonic) / one_pending;
65}
66
6367// Spawns a new thread for the task. This is appropriate when the callee
6468// delegates all work.
6569pub fn spawnManager(