authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-07-17 20:26:07-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-07-20 10:38:39-07:00
log33b10abaf607d794c9fc379248c8b97c6195c053
tree2f9cdc3b0aaa22af2f387ecc46f46fb94dc60af3
parent384545acbce457163c593ee0b3bf8e4cd76a319a

std.Io: add asyncConcurrent and asyncParallel


4 files changed, 328 insertions(+), 307 deletions(-)

lib/std/Io.zig+120-1
......@@ -934,6 +934,32 @@ pub const VTable = struct {
934934 context_alignment: std.mem.Alignment,
935935 start: *const fn (context: *const anyopaque, result: *anyopaque) void,
936936 ) ?*AnyFuture,
937 /// Returning `null` indicates resource allocation failed.
938 ///
939 /// Thread-safe.
940 asyncConcurrent: *const fn (
941 /// Corresponds to `Io.userdata`.
942 userdata: ?*anyopaque,
943 result_len: usize,
944 result_alignment: std.mem.Alignment,
945 /// Copied and then passed to `start`.
946 context: []const u8,
947 context_alignment: std.mem.Alignment,
948 start: *const fn (context: *const anyopaque, result: *anyopaque) void,
949 ) ?*AnyFuture,
950 /// Returning `null` indicates resource allocation failed.
951 ///
952 /// Thread-safe.
953 asyncParallel: *const fn (
954 /// Corresponds to `Io.userdata`.
955 userdata: ?*anyopaque,
956 result_len: usize,
957 result_alignment: std.mem.Alignment,
958 /// Copied and then passed to `start`.
959 context: []const u8,
960 context_alignment: std.mem.Alignment,
961 start: *const fn (context: *const anyopaque, result: *anyopaque) void,
962 ) ?*AnyFuture,
937963 /// Executes `start` asynchronously in a manner such that it cleans itself
938964 /// up. This mode does not support results, await, or cancel.
939965 ///
......@@ -1491,7 +1517,18 @@ pub fn Queue(Elem: type) type {
14911517
14921518/// Calls `function` with `args`, such that the return value of the function is
14931519/// not guaranteed to be available until `await` is called.
1494pub fn async(io: Io, function: anytype, args: std.meta.ArgsTuple(@TypeOf(function))) Future(@typeInfo(@TypeOf(function)).@"fn".return_type.?) {
1520///
1521/// `function` *may* be called immediately, before `async` returns. This has
1522/// weaker guarantees than `asyncConcurrent` and `asyncParallel`, making it the
1523/// most portable and reusable among the async family functions.
1524///
1525/// See also:
1526/// * `asyncDetached`
1527pub fn async(
1528 io: Io,
1529 function: anytype,
1530 args: std.meta.ArgsTuple(@TypeOf(function)),
1531) Future(@typeInfo(@TypeOf(function)).@"fn".return_type.?) {
14951532 const Result = @typeInfo(@TypeOf(function)).@"fn".return_type.?;
14961533 const Args = @TypeOf(args);
14971534 const TypeErased = struct {
......@@ -1513,8 +1550,86 @@ pub fn async(io: Io, function: anytype, args: std.meta.ArgsTuple(@TypeOf(functio
15131550 return future;
15141551}
15151552
1553/// Calls `function` with `args`, such that the return value of the function is
1554/// not guaranteed to be available until `await` is called, passing control
1555/// flow back to the caller while waiting for any `Io` operations.
1556///
1557/// This has a weaker guarantee than `asyncParallel`, making it more portable
1558/// and reusable, however it has stronger guarantee than `async`, placing
1559/// restrictions on what kind of `Io` implementations are supported. By calling
1560/// `async` instead, one allows, for example, stackful single-threaded blocking I/O.
1561pub fn asyncConcurrent(
1562 io: Io,
1563 function: anytype,
1564 args: std.meta.ArgsTuple(@TypeOf(function)),
1565) error{OutOfMemory}!Future(@typeInfo(@TypeOf(function)).@"fn".return_type.?) {
1566 const Result = @typeInfo(@TypeOf(function)).@"fn".return_type.?;
1567 const Args = @TypeOf(args);
1568 const TypeErased = struct {
1569 fn start(context: *const anyopaque, result: *anyopaque) void {
1570 const args_casted: *const Args = @alignCast(@ptrCast(context));
1571 const result_casted: *Result = @ptrCast(@alignCast(result));
1572 result_casted.* = @call(.auto, function, args_casted.*);
1573 }
1574 };
1575 var future: Future(Result) = undefined;
1576 future.any_future = io.vtable.asyncConcurrent(
1577 io.userdata,
1578 @sizeOf(Result),
1579 .of(Result),
1580 @ptrCast((&args)[0..1]),
1581 .of(Args),
1582 TypeErased.start,
1583 );
1584 return future;
1585}
1586
1587/// Calls `function` with `args`, such that the return value of the function is
1588/// not guaranteed to be available until `await` is called, while simultaneously
1589/// passing control flow back to the caller.
1590///
1591/// This has the strongest guarantees of all async family functions, placing
1592/// the most restrictions on what kind of `Io` implementations are supported.
1593/// By calling `asyncConcurrent` instead, one allows, for example,
1594/// stackful single-threaded non-blocking I/O.
1595///
1596/// See also:
1597/// * `asyncConcurrent`
1598/// * `async`
1599pub fn asyncParallel(
1600 io: Io,
1601 function: anytype,
1602 args: std.meta.ArgsTuple(@TypeOf(function)),
1603) error{OutOfMemory}!Future(@typeInfo(@TypeOf(function)).@"fn".return_type.?) {
1604 const Result = @typeInfo(@TypeOf(function)).@"fn".return_type.?;
1605 const Args = @TypeOf(args);
1606 const TypeErased = struct {
1607 fn start(context: *const anyopaque, result: *anyopaque) void {
1608 const args_casted: *const Args = @alignCast(@ptrCast(context));
1609 const result_casted: *Result = @ptrCast(@alignCast(result));
1610 result_casted.* = @call(.auto, function, args_casted.*);
1611 }
1612 };
1613 var future: Future(Result) = undefined;
1614 future.any_future = io.vtable.asyncConcurrent(
1615 io.userdata,
1616 @ptrCast((&future.result)[0..1]),
1617 .of(Result),
1618 @ptrCast((&args)[0..1]),
1619 .of(Args),
1620 TypeErased.start,
1621 );
1622 return future;
1623}
1624
15161625/// Calls `function` with `args` asynchronously. The resource cleans itself up
15171626/// when the function returns. Does not support await, cancel, or a return value.
1627///
1628/// `function` *may* be called immediately, before `async` returns.
1629///
1630/// See also:
1631/// * `async`
1632/// * `asyncConcurrent`
15181633pub fn asyncDetached(io: Io, function: anytype, args: std.meta.ArgsTuple(@TypeOf(function))) void {
15191634 const Args = @TypeOf(args);
15201635 const TypeErased = struct {
......@@ -1526,6 +1641,10 @@ pub fn asyncDetached(io: Io, function: anytype, args: std.meta.ArgsTuple(@TypeOf
15261641 io.vtable.asyncDetached(io.userdata, @ptrCast((&args)[0..1]), .of(Args), TypeErased.start);
15271642}
15281643
1644pub fn cancelRequested(io: Io) bool {
1645 return io.vtable.cancelRequested(io.userdata);
1646}
1647
15291648pub fn now(io: Io, clockid: std.posix.clockid_t) ClockGetTimeError!Timestamp {
15301649 return io.vtable.now(io.userdata, clockid);
15311650}
lib/std/Io/EventLoop.zig+35-5
......@@ -139,6 +139,8 @@ pub fn io(el: *EventLoop) Io {
139139 .userdata = el,
140140 .vtable = &.{
141141 .async = async,
142 .asyncConcurrent = asyncConcurrent,
143 .asyncParallel = asyncParallel,
142144 .await = await,
143145 .asyncDetached = asyncDetached,
144146 .select = select,
......@@ -876,17 +878,28 @@ fn async(
876878 context: []const u8,
877879 context_alignment: Alignment,
878880 start: *const fn (context: *const anyopaque, result: *anyopaque) void,
881) ?*std.Io.AnyFuture {
882 return asyncConcurrent(userdata, result.len, result_alignment, context, context_alignment, start) orelse {
883 start(context.ptr, result.ptr);
884 return null;
885 };
886}
887
888fn asyncConcurrent(
889 userdata: ?*anyopaque,
890 result_len: usize,
891 result_alignment: Alignment,
892 context: []const u8,
893 context_alignment: Alignment,
894 start: *const fn (context: *const anyopaque, result: *anyopaque) void,
879895) ?*std.Io.AnyFuture {
880896 assert(result_alignment.compare(.lte, Fiber.max_result_align)); // TODO
881897 assert(context_alignment.compare(.lte, Fiber.max_context_align)); // TODO
882 assert(result.len <= Fiber.max_result_size); // TODO
898 assert(result_len <= Fiber.max_result_size); // TODO
883899 assert(context.len <= Fiber.max_context_size); // TODO
884900
885901 const event_loop: *EventLoop = @alignCast(@ptrCast(userdata));
886 const fiber = Fiber.allocate(event_loop) catch {
887 start(context.ptr, result.ptr);
888 return null;
889 };
902 const fiber = Fiber.allocate(event_loop) catch return null;
890903 std.log.debug("allocated {*}", .{fiber});
891904
892905 const closure: *AsyncClosure = .fromFiber(fiber);
......@@ -925,6 +938,23 @@ fn async(
925938 return @ptrCast(fiber);
926939}
927940
941fn asyncParallel(
942 userdata: ?*anyopaque,
943 result_len: usize,
944 result_alignment: Alignment,
945 context: []const u8,
946 context_alignment: Alignment,
947 start: *const fn (context: *const anyopaque, result: *anyopaque) void,
948) ?*std.Io.AnyFuture {
949 _ = userdata;
950 _ = result_len;
951 _ = result_alignment;
952 _ = context;
953 _ = context_alignment;
954 _ = start;
955 @panic("TODO");
956}
957
928958const DetachedClosure = struct {
929959 event_loop: *EventLoop,
930960 fiber: *Fiber,
lib/std/Io/ThreadPool.zig+171-301
......@@ -6,331 +6,88 @@ const WaitGroup = std.Thread.WaitGroup;
66const Io = std.Io;
77const Pool = @This();
88
9/// Must be a thread-safe allocator.
10allocator: std.mem.Allocator,
9/// Thread-safe.
10allocator: Allocator,
1111mutex: std.Thread.Mutex = .{},
1212cond: std.Thread.Condition = .{},
1313run_queue: std.SinglyLinkedList = .{},
14is_running: bool = true,
14join_requested: bool = false,
1515threads: std.ArrayListUnmanaged(std.Thread),
16ids: if (builtin.single_threaded) struct {
17 inline fn deinit(_: @This(), _: std.mem.Allocator) void {}
18 fn getIndex(_: @This(), _: std.Thread.Id) usize {
19 return 0;
20 }
21} else std.AutoArrayHashMapUnmanaged(std.Thread.Id, void),
2216stack_size: usize,
17cpu_count: std.Thread.CpuCountError!usize,
18parallel_count: usize,
2319
2420threadlocal var current_closure: ?*AsyncClosure = null;
2521
2622pub const Runnable = struct {
27 runFn: RunProto,
23 start: Start,
2824 node: std.SinglyLinkedList.Node = .{},
29};
30
31pub const RunProto = *const fn (*Runnable, id: ?usize) void;
25 is_parallel: bool,
3226
33pub const Options = struct {
34 allocator: std.mem.Allocator,
35 n_jobs: ?usize = null,
36 track_ids: bool = false,
37 stack_size: usize = std.Thread.SpawnConfig.default_stack_size,
27 pub const Start = *const fn (*Runnable) void;
3828};
3929
40pub fn init(pool: *Pool, options: Options) !void {
41 const gpa = options.allocator;
42 const thread_count = options.n_jobs orelse @max(1, std.Thread.getCpuCount() catch 1);
43 const threads = try gpa.alloc(std.Thread, thread_count);
44 errdefer gpa.free(threads);
30pub const InitError = std.Thread.CpuCountError || Allocator.Error;
4531
46 pool.* = .{
32pub fn init(gpa: Allocator) Pool {
33 var pool: Pool = .{
4734 .allocator = gpa,
48 .threads = .initBuffer(threads),
49 .ids = .{},
50 .stack_size = options.stack_size,
35 .threads = .empty,
36 .stack_size = std.Thread.SpawnConfig.default_stack_size,
37 .cpu_count = std.Thread.getCpuCount(),
38 .parallel_count = 0,
5139 };
52
53 if (builtin.single_threaded) return;
54
55 if (options.track_ids) {
56 try pool.ids.ensureTotalCapacity(gpa, 1 + thread_count);
57 pool.ids.putAssumeCapacityNoClobber(std.Thread.getCurrentId(), {});
58 }
40 if (pool.cpu_count) |n| {
41 pool.threads.ensureTotalCapacityPrecise(gpa, n - 1) catch {};
42 } else |_| {}
43 return pool;
5944}
6045
6146pub fn deinit(pool: *Pool) void {
6247 const gpa = pool.allocator;
6348 pool.join();
6449 pool.threads.deinit(gpa);
65 pool.ids.deinit(gpa);
6650 pool.* = undefined;
6751}
6852
6953fn join(pool: *Pool) void {
7054 if (builtin.single_threaded) return;
71
7255 {
7356 pool.mutex.lock();
7457 defer pool.mutex.unlock();
75
76 // ensure future worker threads exit the dequeue loop
77 pool.is_running = false;
58 pool.join_requested = true;
7859 }
79
80 // wake up any sleeping threads (this can be done outside the mutex)
81 // then wait for all the threads we know are spawned to complete.
8260 pool.cond.broadcast();
8361 for (pool.threads.items) |thread| thread.join();
8462}
8563
86/// Runs `func` in the thread pool, calling `WaitGroup.start` beforehand, and
87/// `WaitGroup.finish` after it returns.
88///
89/// In the case that queuing the function call fails to allocate memory, or the
90/// target is single-threaded, the function is called directly.
91pub fn spawnWg(pool: *Pool, wait_group: *WaitGroup, comptime func: anytype, args: anytype) void {
92 wait_group.start();
93
94 if (builtin.single_threaded) {
95 @call(.auto, func, args);
96 wait_group.finish();
97 return;
98 }
99
100 const Args = @TypeOf(args);
101 const Closure = struct {
102 arguments: Args,
103 pool: *Pool,
104 runnable: Runnable = .{ .runFn = runFn },
105 wait_group: *WaitGroup,
106
107 fn runFn(runnable: *Runnable, _: ?usize) void {
108 const closure: *@This() = @alignCast(@fieldParentPtr("runnable", runnable));
109 @call(.auto, func, closure.arguments);
110 closure.wait_group.finish();
111 closure.pool.allocator.destroy(closure);
112 }
113 };
114
115 pool.mutex.lock();
116
117 const gpa = pool.allocator;
118 const closure = gpa.create(Closure) catch {
119 pool.mutex.unlock();
120 @call(.auto, func, args);
121 wait_group.finish();
122 return;
123 };
124 closure.* = .{
125 .arguments = args,
126 .pool = pool,
127 .wait_group = wait_group,
128 };
129
130 pool.run_queue.prepend(&closure.runnable.node);
131
132 if (pool.threads.items.len < pool.threads.capacity) {
133 pool.threads.addOneAssumeCapacity().* = std.Thread.spawn(.{
134 .stack_size = pool.stack_size,
135 .allocator = gpa,
136 }, worker, .{pool}) catch t: {
137 pool.threads.items.len -= 1;
138 break :t undefined;
139 };
140 }
141
142 pool.mutex.unlock();
143 pool.cond.signal();
144}
145
146/// Runs `func` in the thread pool, calling `WaitGroup.start` beforehand, and
147/// `WaitGroup.finish` after it returns.
148///
149/// The first argument passed to `func` is a dense `usize` thread id, the rest
150/// of the arguments are passed from `args`. Requires the pool to have been
151/// initialized with `.track_ids = true`.
152///
153/// In the case that queuing the function call fails to allocate memory, or the
154/// target is single-threaded, the function is called directly.
155pub fn spawnWgId(pool: *Pool, wait_group: *WaitGroup, comptime func: anytype, args: anytype) void {
156 wait_group.start();
157
158 if (builtin.single_threaded) {
159 @call(.auto, func, .{0} ++ args);
160 wait_group.finish();
161 return;
162 }
163
164 const Args = @TypeOf(args);
165 const Closure = struct {
166 arguments: Args,
167 pool: *Pool,
168 runnable: Runnable = .{ .runFn = runFn },
169 wait_group: *WaitGroup,
170
171 fn runFn(runnable: *Runnable, id: ?usize) void {
172 const closure: *@This() = @alignCast(@fieldParentPtr("runnable", runnable));
173 @call(.auto, func, .{id.?} ++ closure.arguments);
174 closure.wait_group.finish();
175 closure.pool.allocator.destroy(closure);
176 }
177 };
178
179 pool.mutex.lock();
180
181 const gpa = pool.allocator;
182 const closure = gpa.create(Closure) catch {
183 const id: ?usize = pool.ids.getIndex(std.Thread.getCurrentId());
184 pool.mutex.unlock();
185 @call(.auto, func, .{id.?} ++ args);
186 wait_group.finish();
187 return;
188 };
189 closure.* = .{
190 .arguments = args,
191 .pool = pool,
192 .wait_group = wait_group,
193 };
194
195 pool.run_queue.prepend(&closure.runnable.node);
196
197 if (pool.threads.items.len < pool.threads.capacity) {
198 pool.threads.addOneAssumeCapacity().* = std.Thread.spawn(.{
199 .stack_size = pool.stack_size,
200 .allocator = gpa,
201 }, worker, .{pool}) catch t: {
202 pool.threads.items.len -= 1;
203 break :t undefined;
204 };
205 }
206
207 pool.mutex.unlock();
208 pool.cond.signal();
209}
210
211pub fn spawn(pool: *Pool, comptime func: anytype, args: anytype) void {
212 if (builtin.single_threaded) {
213 @call(.auto, func, args);
214 return;
215 }
216
217 const Args = @TypeOf(args);
218 const Closure = struct {
219 arguments: Args,
220 pool: *Pool,
221 runnable: Runnable = .{ .runFn = runFn },
222
223 fn runFn(runnable: *Runnable, _: ?usize) void {
224 const closure: *@This() = @alignCast(@fieldParentPtr("runnable", runnable));
225 @call(.auto, func, closure.arguments);
226 closure.pool.allocator.destroy(closure);
227 }
228 };
229
230 pool.mutex.lock();
231
232 const gpa = pool.allocator;
233 const closure = gpa.create(Closure) catch {
234 pool.mutex.unlock();
235 @call(.auto, func, args);
236 return;
237 };
238 closure.* = .{
239 .arguments = args,
240 .pool = pool,
241 };
242
243 pool.run_queue.prepend(&closure.runnable.node);
244
245 if (pool.threads.items.len < pool.threads.capacity) {
246 pool.threads.addOneAssumeCapacity().* = std.Thread.spawn(.{
247 .stack_size = pool.stack_size,
248 .allocator = gpa,
249 }, worker, .{pool}) catch t: {
250 pool.threads.items.len -= 1;
251 break :t undefined;
252 };
253 }
254
255 pool.mutex.unlock();
256 pool.cond.signal();
257}
258
259test spawn {
260 const TestFn = struct {
261 fn checkRun(completed: *bool) void {
262 completed.* = true;
263 }
264 };
265
266 var completed: bool = false;
267
268 {
269 var pool: Pool = undefined;
270 try pool.init(.{
271 .allocator = std.testing.allocator,
272 });
273 defer pool.deinit();
274 pool.spawn(TestFn.checkRun, .{&completed});
275 }
276
277 try std.testing.expectEqual(true, completed);
278}
279
28064fn worker(pool: *Pool) void {
28165 pool.mutex.lock();
28266 defer pool.mutex.unlock();
28367
284 const id: ?usize = if (pool.ids.count() > 0) @intCast(pool.ids.count()) else null;
285 if (id) |_| pool.ids.putAssumeCapacityNoClobber(std.Thread.getCurrentId(), {});
286
28768 while (true) {
28869 while (pool.run_queue.popFirst()) |run_node| {
289 // Temporarily unlock the mutex in order to execute the run_node
290 pool.mutex.unlock();
291 defer pool.mutex.lock();
292
293 const runnable: *Runnable = @fieldParentPtr("node", run_node);
294 runnable.runFn(runnable, id);
295 }
296
297 // Stop executing instead of waiting if the thread pool is no longer running.
298 if (pool.is_running) {
299 pool.cond.wait(&pool.mutex);
300 } else {
301 break;
302 }
303 }
304}
305
306pub fn waitAndWork(pool: *Pool, wait_group: *WaitGroup) void {
307 var id: ?usize = null;
308
309 while (!wait_group.isDone()) {
310 pool.mutex.lock();
311 if (pool.run_queue.popFirst()) |run_node| {
312 id = id orelse pool.ids.getIndex(std.Thread.getCurrentId());
31370 pool.mutex.unlock();
31471 const runnable: *Runnable = @fieldParentPtr("node", run_node);
315 runnable.runFn(runnable, id);
316 continue;
72 runnable.start(runnable);
73 pool.mutex.lock();
74 if (runnable.is_parallel) {
75 // TODO also pop thread and join sometimes
76 pool.parallel_count -= 1;
77 }
31778 }
318
319 pool.mutex.unlock();
320 wait_group.wait();
321 return;
79 if (pool.join_requested) break;
80 pool.cond.wait(&pool.mutex);
32281 }
32382}
32483
325pub fn getIdCount(pool: *Pool) usize {
326 return @intCast(1 + pool.threads.items.len);
327}
328
32984pub fn io(pool: *Pool) Io {
33085 return .{
33186 .userdata = pool,
33287 .vtable = &.{
33388 .async = async,
89 .asyncConcurrent = asyncParallel,
90 .asyncParallel = asyncParallel,
33491 .await = await,
33592 .asyncDetached = asyncDetached,
33693 .cancel = cancel,
......@@ -357,7 +114,7 @@ pub fn io(pool: *Pool) Io {
357114
358115const AsyncClosure = struct {
359116 func: *const fn (context: *anyopaque, result: *anyopaque) void,
360 runnable: Runnable = .{ .runFn = runFn },
117 runnable: Runnable,
361118 reset_event: std.Thread.ResetEvent,
362119 select_condition: ?*std.Thread.ResetEvent,
363120 cancel_tid: std.Thread.Id,
......@@ -375,7 +132,7 @@ const AsyncClosure = struct {
375132 else => @compileError("unsupported std.Thread.Id: " ++ @typeName(std.Thread.Id)),
376133 };
377134
378 fn runFn(runnable: *Pool.Runnable, _: ?usize) void {
135 fn start(runnable: *Runnable) void {
379136 const closure: *AsyncClosure = @alignCast(@fieldParentPtr("runnable", runnable));
380137 const tid = std.Thread.getCurrentId();
381138 if (@cmpxchgStrong(
......@@ -387,6 +144,7 @@ const AsyncClosure = struct {
387144 .acquire,
388145 )) |cancel_tid| {
389146 assert(cancel_tid == canceling_tid);
147 closure.reset_event.set();
390148 return;
391149 }
392150 current_closure = closure;
......@@ -438,9 +196,13 @@ const AsyncClosure = struct {
438196
439197 fn waitAndFree(closure: *AsyncClosure, gpa: Allocator, result: []u8) void {
440198 closure.reset_event.wait();
441 const base: [*]align(@alignOf(AsyncClosure)) u8 = @ptrCast(closure);
442199 @memcpy(result, closure.resultPointer()[0..result.len]);
443 gpa.free(base[0 .. closure.result_offset + result.len]);
200 free(closure, gpa, result.len);
201 }
202
203 fn free(closure: *AsyncClosure, gpa: Allocator, result_len: usize) void {
204 const base: [*]align(@alignOf(AsyncClosure)) u8 = @ptrCast(closure);
205 gpa.free(base[0 .. closure.result_offset + result_len]);
444206 }
445207};
446208
......@@ -452,18 +214,26 @@ fn async(
452214 context_alignment: std.mem.Alignment,
453215 start: *const fn (context: *const anyopaque, result: *anyopaque) void,
454216) ?*Io.AnyFuture {
217 if (builtin.single_threaded) {
218 start(context.ptr, result.ptr);
219 return null;
220 }
455221 const pool: *Pool = @alignCast(@ptrCast(userdata));
456 pool.mutex.lock();
457
222 const cpu_count = pool.cpu_count catch {
223 return asyncParallel(userdata, result.len, result_alignment, context, context_alignment, start) orelse {
224 start(context.ptr, result.ptr);
225 return null;
226 };
227 };
458228 const gpa = pool.allocator;
459229 const context_offset = context_alignment.forward(@sizeOf(AsyncClosure));
460230 const result_offset = result_alignment.forward(context_offset + context.len);
461231 const n = result_offset + result.len;
462232 const closure: *AsyncClosure = @alignCast(@ptrCast(gpa.alignedAlloc(u8, .of(AsyncClosure), n) catch {
463 pool.mutex.unlock();
464233 start(context.ptr, result.ptr);
465234 return null;
466235 }));
236
467237 closure.* = .{
468238 .func = start,
469239 .context_offset = context_offset,
......@@ -471,37 +241,124 @@ fn async(
471241 .reset_event = .{},
472242 .cancel_tid = 0,
473243 .select_condition = null,
244 .runnable = .{
245 .start = AsyncClosure.start,
246 .is_parallel = false,
247 },
474248 };
249
475250 @memcpy(closure.contextPointer()[0..context.len], context);
251
252 pool.mutex.lock();
253
254 const thread_capacity = cpu_count - 1 + pool.parallel_count;
255
256 pool.threads.ensureTotalCapacityPrecise(gpa, thread_capacity) catch {
257 pool.mutex.unlock();
258 closure.free(gpa, result.len);
259 start(context.ptr, result.ptr);
260 return null;
261 };
262
476263 pool.run_queue.prepend(&closure.runnable.node);
477264
478 if (pool.threads.items.len < pool.threads.capacity) {
479 pool.threads.addOneAssumeCapacity().* = std.Thread.spawn(.{
480 .stack_size = pool.stack_size,
481 .allocator = gpa,
482 }, worker, .{pool}) catch t: {
483 pool.threads.items.len -= 1;
484 break :t undefined;
265 if (pool.threads.items.len < thread_capacity) {
266 const thread = std.Thread.spawn(.{ .stack_size = pool.stack_size }, worker, .{pool}) catch {
267 if (pool.threads.items.len == 0) {
268 assert(pool.run_queue.popFirst() == &closure.runnable.node);
269 pool.mutex.unlock();
270 closure.free(gpa, result.len);
271 start(context.ptr, result.ptr);
272 return null;
273 }
274 // Rely on other workers to do it.
275 pool.mutex.unlock();
276 pool.cond.signal();
277 return @ptrCast(closure);
485278 };
279 pool.threads.appendAssumeCapacity(thread);
486280 }
487281
488282 pool.mutex.unlock();
489283 pool.cond.signal();
284 return @ptrCast(closure);
285}
286
287fn asyncParallel(
288 userdata: ?*anyopaque,
289 result_len: usize,
290 result_alignment: std.mem.Alignment,
291 context: []const u8,
292 context_alignment: std.mem.Alignment,
293 start: *const fn (context: *const anyopaque, result: *anyopaque) void,
294) ?*Io.AnyFuture {
295 if (builtin.single_threaded) return null;
296
297 const pool: *Pool = @alignCast(@ptrCast(userdata));
298 const cpu_count = pool.cpu_count catch 1;
299 const gpa = pool.allocator;
300 const context_offset = context_alignment.forward(@sizeOf(AsyncClosure));
301 const result_offset = result_alignment.forward(context_offset + context.len);
302 const n = result_offset + result_len;
303 const closure: *AsyncClosure = @alignCast(@ptrCast(gpa.alignedAlloc(u8, .of(AsyncClosure), n) catch return null));
304
305 closure.* = .{
306 .func = start,
307 .context_offset = context_offset,
308 .result_offset = result_offset,
309 .reset_event = .{},
310 .cancel_tid = 0,
311 .select_condition = null,
312 .runnable = .{
313 .start = AsyncClosure.start,
314 .is_parallel = true,
315 },
316 };
317 @memcpy(closure.contextPointer()[0..context.len], context);
318
319 pool.mutex.lock();
320
321 pool.parallel_count += 1;
322 const thread_capacity = cpu_count - 1 + pool.parallel_count;
323
324 pool.threads.ensureTotalCapacity(gpa, thread_capacity) catch {
325 pool.mutex.unlock();
326 closure.free(gpa, result_len);
327 return null;
328 };
329
330 pool.run_queue.prepend(&closure.runnable.node);
331
332 if (pool.threads.items.len < thread_capacity) {
333 const thread = std.Thread.spawn(.{ .stack_size = pool.stack_size }, worker, .{pool}) catch {
334 assert(pool.run_queue.popFirst() == &closure.runnable.node);
335 pool.mutex.unlock();
336 closure.free(gpa, result_len);
337 return null;
338 };
339 pool.threads.appendAssumeCapacity(thread);
340 }
490341
342 pool.mutex.unlock();
343 pool.cond.signal();
491344 return @ptrCast(closure);
492345}
493346
494347const DetachedClosure = struct {
495348 pool: *Pool,
496349 func: *const fn (context: *anyopaque) void,
497 runnable: Runnable = .{ .runFn = runFn },
350 runnable: Runnable,
498351 context_alignment: std.mem.Alignment,
499352 context_len: usize,
500353
501 fn runFn(runnable: *Pool.Runnable, _: ?usize) void {
354 fn start(runnable: *Runnable) void {
502355 const closure: *DetachedClosure = @alignCast(@fieldParentPtr("runnable", runnable));
503356 closure.func(closure.contextPointer());
504357 const gpa = closure.pool.allocator;
358 free(closure, gpa);
359 }
360
361 fn free(closure: *DetachedClosure, gpa: Allocator) void {
505362 const base: [*]align(@alignOf(DetachedClosure)) u8 = @ptrCast(closure);
506363 gpa.free(base[0..contextEnd(closure.context_alignment, closure.context_len)]);
507364 }
......@@ -526,33 +383,46 @@ fn asyncDetached(
526383 context_alignment: std.mem.Alignment,
527384 start: *const fn (context: *const anyopaque) void,
528385) void {
386 if (builtin.single_threaded) return start(context.ptr);
529387 const pool: *Pool = @alignCast(@ptrCast(userdata));
530 pool.mutex.lock();
531
388 const cpu_count = pool.cpu_count catch 1;
532389 const gpa = pool.allocator;
533390 const n = DetachedClosure.contextEnd(context_alignment, context.len);
534391 const closure: *DetachedClosure = @alignCast(@ptrCast(gpa.alignedAlloc(u8, .of(DetachedClosure), n) catch {
535 pool.mutex.unlock();
536 start(context.ptr);
537 return;
392 return start(context.ptr);
538393 }));
539394 closure.* = .{
540395 .pool = pool,
541396 .func = start,
542397 .context_alignment = context_alignment,
543398 .context_len = context.len,
399 .runnable = .{
400 .start = DetachedClosure.start,
401 .is_parallel = false,
402 },
544403 };
545404 @memcpy(closure.contextPointer()[0..context.len], context);
405
406 pool.mutex.lock();
407
408 const thread_capacity = cpu_count - 1 + pool.parallel_count;
409
410 pool.threads.ensureTotalCapacityPrecise(gpa, thread_capacity) catch {
411 pool.mutex.unlock();
412 closure.free(gpa);
413 return start(context.ptr);
414 };
415
546416 pool.run_queue.prepend(&closure.runnable.node);
547417
548 if (pool.threads.items.len < pool.threads.capacity) {
549 pool.threads.addOneAssumeCapacity().* = std.Thread.spawn(.{
550 .stack_size = pool.stack_size,
551 .allocator = gpa,
552 }, worker, .{pool}) catch t: {
553 pool.threads.items.len -= 1;
554 break :t undefined;
418 if (pool.threads.items.len < thread_capacity) {
419 const thread = std.Thread.spawn(.{ .stack_size = pool.stack_size }, worker, .{pool}) catch {
420 assert(pool.run_queue.popFirst() == &closure.runnable.node);
421 pool.mutex.unlock();
422 closure.free(gpa);
423 return start(context.ptr);
555424 };
425 pool.threads.appendAssumeCapacity(thread);
556426 }
557427
558428 pool.mutex.unlock();
lib/std/Thread.zig+2
......@@ -384,6 +384,8 @@ pub const CpuCountError = error{
384384};
385385
386386/// Returns the platforms view on the number of logical CPU cores available.
387///
388/// Returned value guaranteed to be >= 1.
387389pub fn getCpuCount() CpuCountError!usize {
388390 return try Impl.getCpuCount();
389391}