| ... | @@ -22,12 +22,30 @@ mutex: std.Thread.Mutex = .{}, | ... | @@ -22,12 +22,30 @@ mutex: std.Thread.Mutex = .{}, |
| 22 | cond: std.Thread.Condition = .{}, | 22 | cond: std.Thread.Condition = .{}, |
| 23 | run_queue: std.SinglyLinkedList = .{}, | 23 | run_queue: std.SinglyLinkedList = .{}, |
| 24 | join_requested: bool = false, | 24 | join_requested: bool = false, |
| 25 | threads: std.ArrayList(std.Thread), | | |
| 26 | stack_size: usize, | 25 | stack_size: usize, |
| 27 | cpu_count: usize, // 0 means no limit | 26 | /// All threads are spawned detached; this is how we wait until they all exit. |
| 28 | concurrency_limit: usize, // 0 means no limit | 27 | wait_group: std.Thread.WaitGroup = .{}, |
| 29 | available_thread_count: usize = 0, | 28 | /// Maximum thread pool size (excluding main thread) when dispatching async |
| 30 | one_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. |
| | 35 | async_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`. |
| | 42 | concurrent_limit: Io.Limit = .unlimited, |
| | 43 | /// Error from calling `std.Thread.getCpuCount` in `init`. |
| | 44 | cpu_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`. |
| | 48 | busy_count: usize = 0, |
| 31 | | 49 | |
| 32 | wsa: if (is_windows) Wsa else struct {} = .{}, | 50 | wsa: if (is_windows) Wsa else struct {} = .{}, |
| 33 | | 51 | |
| ... | @@ -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; |
| 105 | | 123 | |
| | 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 | }; |
| 116 | | 135 | |
| 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 blocking | 137 | // 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. |
| 141 | pub const init_single_threaded: Threaded = .{ | 158 | pub 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 | }; |
| 151 | | 168 | |
| 152 | pub fn deinit(t: *Threaded) void { | 169 | pub 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 | } |
| 176 | | 191 | |
| 177 | fn worker(t: *Threaded) void { | 192 | fn 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(); |
| 180 | | 197 | |
| ... | @@ -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 | } |
| 193 | | 210 | |
| 194 | fn 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 | | | |
| 201 | pub fn io(t: *Threaded) Io { | 211 | pub 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( |
| 500 | | 510 | |
| 501 | t.mutex.lock(); | 511 | t.mutex.lock(); |
| 502 | | 512 | |
| 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 | } | | |
| 510 | | 514 | |
| 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 | } |
| 517 | | 521 | |
| 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 | }; |
| 528 | | 535 | thread.detach(); |
| 529 | t.threads.appendAssumeCapacity(thread); | | |
| 530 | } else { | | |
| 531 | t.available_thread_count -= 1; | | |
| 532 | } | 536 | } |
| 533 | | 537 | |
| 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)); |
| 551 | | 555 | |
| 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); |
| 557 | | 560 | |
| 558 | t.mutex.lock(); | 561 | t.mutex.lock(); |
| 559 | defer t.mutex.unlock(); | 562 | defer t.mutex.unlock(); |
| 560 | | 563 | |
| 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 | } | | |
| 568 | | 565 | |
| 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(); |
| 572 | | 576 | |
| 573 | const thread = std.Thread.spawn(.{ .stack_size = t.stack_size }, worker, .{t}) catch | 577 | const thread = std.Thread.spawn(.{ .stack_size = t.stack_size }, worker, .{t}) catch |
| 574 | return error.ConcurrencyUnavailable; | 578 | return error.ConcurrencyUnavailable; |
| 575 | | 579 | 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 | } |
| 581 | | 581 | |
| 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 | } |
| 596 | | 586 | |
| ... | @@ -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) catch | 682 | const gc = GroupClosure.init(gpa, t, group, context, context_alignment, start) catch |
| 692 | return start(group, context.ptr); | 683 | return start(group, context.ptr); |
| 693 | | 684 | |
| 694 | t.mutex.lock(); | 685 | t.mutex.lock(); |
| 695 | | 686 | |
| 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 | } | | |
| 702 | | 688 | |
| 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 | } |
| 708 | | 694 | |
| 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 | }; |
| 718 | | 707 | thread.detach(); |
| 719 | t.threads.appendAssumeCapacity(thread); | | |
| 720 | } else { | | |
| 721 | t.available_thread_count -= 1; | | |
| 722 | } | 708 | } |
| 723 | | 709 | |
| 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. |