| ... | ... | @@ -22,12 +22,30 @@ mutex: std.Thread.Mutex = .{}, |
| 22 | 22 | cond: std.Thread.Condition = .{}, |
| 23 | 23 | run_queue: std.SinglyLinkedList = .{}, |
| 24 | 24 | join_requested: bool = false, |
| 25 | | threads: std.ArrayList(std.Thread), |
| 26 | 25 | stack_size: usize, |
| 27 | | cpu_count: usize, // 0 means no limit |
| 28 | | concurrency_limit: usize, // 0 means no limit |
| 29 | | available_thread_count: usize = 0, |
| 30 | | one_shot_thread_count: usize = 0, |
| 26 | /// All threads are spawned detached; this is how we wait until they all exit. |
| 27 | wait_group: std.Thread.WaitGroup = .{}, |
| 28 | /// Maximum thread pool size (excluding main thread) when dispatching async |
| 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 | 50 | wsa: if (is_windows) Wsa else struct {} = .{}, |
| 33 | 51 | |
| ... | ... | @@ -103,19 +121,18 @@ pub fn init( |
| 103 | 121 | ) Threaded { |
| 104 | 122 | if (builtin.single_threaded) return .init_single_threaded; |
| 105 | 123 | |
| 124 | const cpu_count = std.Thread.getCpuCount(); |
| 125 | |
| 106 | 126 | var t: Threaded = .{ |
| 107 | 127 | .allocator = gpa, |
| 108 | | .threads = .empty, |
| 109 | 128 | .stack_size = std.Thread.SpawnConfig.default_stack_size, |
| 110 | | .cpu_count = std.Thread.getCpuCount() catch 0, |
| 111 | | .concurrency_limit = 0, |
| 129 | .async_limit = if (cpu_count) |n| .limited(n - 1) else |_| .nothing, |
| 130 | .cpu_count_error = if (cpu_count) |_| null else |e| e, |
| 112 | 131 | .old_sig_io = undefined, |
| 113 | 132 | .old_sig_pipe = undefined, |
| 114 | 133 | .have_signal_handler = false, |
| 115 | 134 | }; |
| 116 | 135 | |
| 117 | | t.threads.ensureTotalCapacity(gpa, t.cpu_count) catch {}; |
| 118 | | |
| 119 | 136 | if (posix.Sigaction != void) { |
| 120 | 137 | // This causes sending `posix.SIG.IO` to thread to interrupt blocking |
| 121 | 138 | // syscalls, returning `posix.E.INTR`. |
| ... | ... | @@ -140,19 +157,17 @@ pub fn init( |
| 140 | 157 | /// * `deinit` is safe, but unnecessary to call. |
| 141 | 158 | pub const init_single_threaded: Threaded = .{ |
| 142 | 159 | .allocator = .failing, |
| 143 | | .threads = .empty, |
| 144 | 160 | .stack_size = std.Thread.SpawnConfig.default_stack_size, |
| 145 | | .cpu_count = 1, |
| 146 | | .concurrency_limit = 0, |
| 161 | .async_limit = .nothing, |
| 162 | .cpu_count_error = null, |
| 163 | .concurrent_limit = .nothing, |
| 147 | 164 | .old_sig_io = undefined, |
| 148 | 165 | .old_sig_pipe = undefined, |
| 149 | 166 | .have_signal_handler = false, |
| 150 | 167 | }; |
| 151 | 168 | |
| 152 | 169 | pub fn deinit(t: *Threaded) void { |
| 153 | | const gpa = t.allocator; |
| 154 | 170 | t.join(); |
| 155 | | t.threads.deinit(gpa); |
| 156 | 171 | if (is_windows and t.wsa.status == .initialized) { |
| 157 | 172 | if (ws2_32.WSACleanup() != 0) recoverableOsBugDetected(); |
| 158 | 173 | } |
| ... | ... | @@ -171,10 +186,12 @@ fn join(t: *Threaded) void { |
| 171 | 186 | t.join_requested = true; |
| 172 | 187 | } |
| 173 | 188 | t.cond.broadcast(); |
| 174 | | for (t.threads.items) |thread| thread.join(); |
| 189 | t.wait_group.wait(); |
| 175 | 190 | } |
| 176 | 191 | |
| 177 | 192 | fn worker(t: *Threaded) void { |
| 193 | defer t.wait_group.finish(); |
| 194 | |
| 178 | 195 | t.mutex.lock(); |
| 179 | 196 | defer t.mutex.unlock(); |
| 180 | 197 | |
| ... | ... | @@ -184,20 +201,13 @@ fn worker(t: *Threaded) void { |
| 184 | 201 | const closure: *Closure = @fieldParentPtr("node", closure_node); |
| 185 | 202 | closure.start(closure); |
| 186 | 203 | t.mutex.lock(); |
| 187 | | t.available_thread_count += 1; |
| 204 | t.busy_count -= 1; |
| 188 | 205 | } |
| 189 | 206 | if (t.join_requested) break; |
| 190 | 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 | 211 | pub fn io(t: *Threaded) Io { |
| 202 | 212 | return .{ |
| 203 | 213 | .userdata = t, |
| ... | ... | @@ -488,7 +498,7 @@ fn async( |
| 488 | 498 | start: *const fn (context: *const anyopaque, result: *anyopaque) void, |
| 489 | 499 | ) ?*Io.AnyFuture { |
| 490 | 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 | 502 | start(context.ptr, result.ptr); |
| 493 | 503 | return null; |
| 494 | 504 | } |
| ... | ... | @@ -500,35 +510,29 @@ fn async( |
| 500 | 510 | |
| 501 | 511 | t.mutex.lock(); |
| 502 | 512 | |
| 503 | | if (t.available_thread_count == 0) { |
| 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 | | } |
| 513 | const busy_count = t.busy_count; |
| 510 | 514 | |
| 511 | | t.threads.ensureUnusedCapacity(gpa, 1) catch { |
| 512 | | t.mutex.unlock(); |
| 513 | | ac.deinit(gpa); |
| 514 | | start(context.ptr, result.ptr); |
| 515 | | return null; |
| 516 | | }; |
| 515 | if (busy_count >= @intFromEnum(t.async_limit)) { |
| 516 | t.mutex.unlock(); |
| 517 | ac.deinit(gpa); |
| 518 | start(context.ptr, result.ptr); |
| 519 | return null; |
| 520 | } |
| 517 | 521 | |
| 518 | | const thread = std.Thread.spawn( |
| 519 | | .{ .stack_size = t.stack_size }, |
| 520 | | worker, |
| 521 | | .{t}, |
| 522 | | ) catch { |
| 522 | t.busy_count = busy_count + 1; |
| 523 | |
| 524 | const pool_size = t.wait_group.value(); |
| 525 | if (pool_size - busy_count == 0) { |
| 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 | 530 | t.mutex.unlock(); |
| 524 | 531 | ac.deinit(gpa); |
| 525 | 532 | start(context.ptr, result.ptr); |
| 526 | 533 | return null; |
| 527 | 534 | }; |
| 528 | | |
| 529 | | t.threads.appendAssumeCapacity(thread); |
| 530 | | } else { |
| 531 | | t.available_thread_count -= 1; |
| 535 | thread.detach(); |
| 532 | 536 | } |
| 533 | 537 | |
| 534 | 538 | t.run_queue.prepend(&ac.closure.node); |
| ... | ... | @@ -550,47 +554,33 @@ fn concurrent( |
| 550 | 554 | const t: *Threaded = @ptrCast(@alignCast(userdata)); |
| 551 | 555 | |
| 552 | 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 | 558 | return error.ConcurrencyUnavailable; |
| 555 | | }; |
| 556 | 559 | errdefer ac.deinit(gpa); |
| 557 | 560 | |
| 558 | 561 | t.mutex.lock(); |
| 559 | 562 | defer t.mutex.unlock(); |
| 560 | 563 | |
| 561 | | // If there's an avilable thread, use it. |
| 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 | | } |
| 564 | const busy_count = t.busy_count; |
| 568 | 565 | |
| 569 | | // If we can spawn a normal worker, spawn it and use it. |
| 570 | | if (t.cpu_count == 0 or t.threads.items.len < t.cpu_count) { |
| 571 | | t.threads.ensureUnusedCapacity(gpa, 1) catch return error.ConcurrencyUnavailable; |
| 566 | if (busy_count >= @intFromEnum(t.concurrent_limit)) |
| 567 | 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 | 577 | const thread = std.Thread.spawn(.{ .stack_size = t.stack_size }, worker, .{t}) catch |
| 574 | 578 | return error.ConcurrencyUnavailable; |
| 575 | | |
| 576 | | t.threads.appendAssumeCapacity(thread); |
| 577 | | t.run_queue.prepend(&ac.closure.node); |
| 578 | | t.cond.signal(); |
| 579 | | return @ptrCast(ac); |
| 579 | thread.detach(); |
| 580 | 580 | } |
| 581 | 581 | |
| 582 | | // If we have a concurrencty limit and we havent' hit it yet, |
| 583 | | // spawn a new one-shot thread. |
| 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 | | |
| 582 | t.run_queue.prepend(&ac.closure.node); |
| 583 | t.cond.signal(); |
| 594 | 584 | return @ptrCast(ac); |
| 595 | 585 | } |
| 596 | 586 | |
| ... | ... | @@ -684,41 +674,37 @@ fn groupAsync( |
| 684 | 674 | context_alignment: std.mem.Alignment, |
| 685 | 675 | start: *const fn (*Io.Group, context: *const anyopaque) void, |
| 686 | 676 | ) void { |
| 687 | | if (builtin.single_threaded) return start(group, context.ptr); |
| 688 | | |
| 689 | 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 | 681 | const gpa = t.allocator; |
| 691 | 682 | const gc = GroupClosure.init(gpa, t, group, context, context_alignment, start) catch |
| 692 | 683 | return start(group, context.ptr); |
| 693 | 684 | |
| 694 | 685 | t.mutex.lock(); |
| 695 | 686 | |
| 696 | | if (t.available_thread_count == 0) { |
| 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 | | } |
| 687 | const busy_count = t.busy_count; |
| 702 | 688 | |
| 703 | | t.threads.ensureUnusedCapacity(gpa, 1) catch { |
| 704 | | t.mutex.unlock(); |
| 705 | | gc.deinit(gpa); |
| 706 | | return start(group, context.ptr); |
| 707 | | }; |
| 689 | if (busy_count >= @intFromEnum(t.async_limit)) { |
| 690 | t.mutex.unlock(); |
| 691 | gc.deinit(gpa); |
| 692 | return start(group, context.ptr); |
| 693 | } |
| 708 | 694 | |
| 709 | | const thread = std.Thread.spawn( |
| 710 | | .{ .stack_size = t.stack_size }, |
| 711 | | worker, |
| 712 | | .{t}, |
| 713 | | ) catch { |
| 695 | t.busy_count = busy_count + 1; |
| 696 | |
| 697 | const pool_size = t.wait_group.value(); |
| 698 | if (pool_size - busy_count == 0) { |
| 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 | 703 | t.mutex.unlock(); |
| 715 | 704 | gc.deinit(gpa); |
| 716 | 705 | return start(group, context.ptr); |
| 717 | 706 | }; |
| 718 | | |
| 719 | | t.threads.appendAssumeCapacity(thread); |
| 720 | | } else { |
| 721 | | t.available_thread_count -= 1; |
| 707 | thread.detach(); |
| 722 | 708 | } |
| 723 | 709 | |
| 724 | 710 | // Append to the group linked list inside the mutex to make `Io.Group.async` thread-safe. |