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 {...@@ -934,6 +934,32 @@ pub const VTable = struct {
934 context_alignment: std.mem.Alignment,934 context_alignment: std.mem.Alignment,
935 start: *const fn (context: *const anyopaque, result: *anyopaque) void,935 start: *const fn (context: *const anyopaque, result: *anyopaque) void,
936 ) ?*AnyFuture,936 ) ?*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,
937 /// Executes `start` asynchronously in a manner such that it cleans itself963 /// Executes `start` asynchronously in a manner such that it cleans itself
938 /// up. This mode does not support results, await, or cancel.964 /// up. This mode does not support results, await, or cancel.
939 ///965 ///
...@@ -1491,7 +1517,18 @@ pub fn Queue(Elem: type) type {...@@ -1491,7 +1517,18 @@ pub fn Queue(Elem: type) type {
14911517
1492/// Calls `function` with `args`, such that the return value of the function is1518/// Calls `function` with `args`, such that the return value of the function is
1493/// not guaranteed to be available until `await` is called.1519/// 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.?) {
1495 const Result = @typeInfo(@TypeOf(function)).@"fn".return_type.?;1532 const Result = @typeInfo(@TypeOf(function)).@"fn".return_type.?;
1496 const Args = @TypeOf(args);1533 const Args = @TypeOf(args);
1497 const TypeErased = struct {1534 const TypeErased = struct {
...@@ -1513,8 +1550,86 @@ pub fn async(io: Io, function: anytype, args: std.meta.ArgsTuple(@TypeOf(functio...@@ -1513,8 +1550,86 @@ pub fn async(io: Io, function: anytype, args: std.meta.ArgsTuple(@TypeOf(functio
1513 return future;1550 return future;
1514}1551}
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
1516/// Calls `function` with `args` asynchronously. The resource cleans itself up1625/// Calls `function` with `args` asynchronously. The resource cleans itself up
1517/// when the function returns. Does not support await, cancel, or a return value.1626/// 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`
1518pub fn asyncDetached(io: Io, function: anytype, args: std.meta.ArgsTuple(@TypeOf(function))) void {1633pub fn asyncDetached(io: Io, function: anytype, args: std.meta.ArgsTuple(@TypeOf(function))) void {
1519 const Args = @TypeOf(args);1634 const Args = @TypeOf(args);
1520 const TypeErased = struct {1635 const TypeErased = struct {
...@@ -1526,6 +1641,10 @@ pub fn asyncDetached(io: Io, function: anytype, args: std.meta.ArgsTuple(@TypeOf...@@ -1526,6 +1641,10 @@ pub fn asyncDetached(io: Io, function: anytype, args: std.meta.ArgsTuple(@TypeOf
1526 io.vtable.asyncDetached(io.userdata, @ptrCast((&args)[0..1]), .of(Args), TypeErased.start);1641 io.vtable.asyncDetached(io.userdata, @ptrCast((&args)[0..1]), .of(Args), TypeErased.start);
1527}1642}
15281643
1644pub fn cancelRequested(io: Io) bool {
1645 return io.vtable.cancelRequested(io.userdata);
1646}
1647
1529pub fn now(io: Io, clockid: std.posix.clockid_t) ClockGetTimeError!Timestamp {1648pub fn now(io: Io, clockid: std.posix.clockid_t) ClockGetTimeError!Timestamp {
1530 return io.vtable.now(io.userdata, clockid);1649 return io.vtable.now(io.userdata, clockid);
1531}1650}
lib/std/Io/EventLoop.zig+35-5
...@@ -139,6 +139,8 @@ pub fn io(el: *EventLoop) Io {...@@ -139,6 +139,8 @@ pub fn io(el: *EventLoop) Io {
139 .userdata = el,139 .userdata = el,
140 .vtable = &.{140 .vtable = &.{
141 .async = async,141 .async = async,
142 .asyncConcurrent = asyncConcurrent,
143 .asyncParallel = asyncParallel,
142 .await = await,144 .await = await,
143 .asyncDetached = asyncDetached,145 .asyncDetached = asyncDetached,
144 .select = select,146 .select = select,
...@@ -876,17 +878,28 @@ fn async(...@@ -876,17 +878,28 @@ fn async(
876 context: []const u8,878 context: []const u8,
877 context_alignment: Alignment,879 context_alignment: Alignment,
878 start: *const fn (context: *const anyopaque, result: *anyopaque) void,880 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,
879) ?*std.Io.AnyFuture {895) ?*std.Io.AnyFuture {
880 assert(result_alignment.compare(.lte, Fiber.max_result_align)); // TODO896 assert(result_alignment.compare(.lte, Fiber.max_result_align)); // TODO
881 assert(context_alignment.compare(.lte, Fiber.max_context_align)); // TODO897 assert(context_alignment.compare(.lte, Fiber.max_context_align)); // TODO
882 assert(result.len <= Fiber.max_result_size); // TODO898 assert(result_len <= Fiber.max_result_size); // TODO
883 assert(context.len <= Fiber.max_context_size); // TODO899 assert(context.len <= Fiber.max_context_size); // TODO
884900
885 const event_loop: *EventLoop = @alignCast(@ptrCast(userdata));901 const event_loop: *EventLoop = @alignCast(@ptrCast(userdata));
886 const fiber = Fiber.allocate(event_loop) catch {902 const fiber = Fiber.allocate(event_loop) catch return null;
887 start(context.ptr, result.ptr);
888 return null;
889 };
890 std.log.debug("allocated {*}", .{fiber});903 std.log.debug("allocated {*}", .{fiber});
891904
892 const closure: *AsyncClosure = .fromFiber(fiber);905 const closure: *AsyncClosure = .fromFiber(fiber);
...@@ -925,6 +938,23 @@ fn async(...@@ -925,6 +938,23 @@ fn async(
925 return @ptrCast(fiber);938 return @ptrCast(fiber);
926}939}
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
928const DetachedClosure = struct {958const DetachedClosure = struct {
929 event_loop: *EventLoop,959 event_loop: *EventLoop,
930 fiber: *Fiber,960 fiber: *Fiber,
lib/std/Io/ThreadPool.zig+171-301
...@@ -6,331 +6,88 @@ const WaitGroup = std.Thread.WaitGroup;...@@ -6,331 +6,88 @@ const WaitGroup = std.Thread.WaitGroup;
6const Io = std.Io;6const Io = std.Io;
7const Pool = @This();7const Pool = @This();
88
9/// Must be a thread-safe allocator.9/// Thread-safe.
10allocator: std.mem.Allocator,10allocator: Allocator,
11mutex: std.Thread.Mutex = .{},11mutex: std.Thread.Mutex = .{},
12cond: std.Thread.Condition = .{},12cond: std.Thread.Condition = .{},
13run_queue: std.SinglyLinkedList = .{},13run_queue: std.SinglyLinkedList = .{},
14is_running: bool = true,14join_requested: bool = false,
15threads: std.ArrayListUnmanaged(std.Thread),15threads: 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),
22stack_size: usize,16stack_size: usize,
17cpu_count: std.Thread.CpuCountError!usize,
18parallel_count: usize,
2319
24threadlocal var current_closure: ?*AsyncClosure = null;20threadlocal var current_closure: ?*AsyncClosure = null;
2521
26pub const Runnable = struct {22pub const Runnable = struct {
27 runFn: RunProto,23 start: Start,
28 node: std.SinglyLinkedList.Node = .{},24 node: std.SinglyLinkedList.Node = .{},
29};25 is_parallel: bool,
30
31pub const RunProto = *const fn (*Runnable, id: ?usize) void;
3226
33pub const Options = struct {27 pub const Start = *const fn (*Runnable) void;
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,
38};28};
3929
40pub fn init(pool: *Pool, options: Options) !void {30pub const InitError = std.Thread.CpuCountError || Allocator.Error;
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);
4531
46 pool.* = .{32pub fn init(gpa: Allocator) Pool {
33 var pool: Pool = .{
47 .allocator = gpa,34 .allocator = gpa,
48 .threads = .initBuffer(threads),35 .threads = .empty,
49 .ids = .{},36 .stack_size = std.Thread.SpawnConfig.default_stack_size,
50 .stack_size = options.stack_size,37 .cpu_count = std.Thread.getCpuCount(),
38 .parallel_count = 0,
51 };39 };
5240 if (pool.cpu_count) |n| {
53 if (builtin.single_threaded) return;41 pool.threads.ensureTotalCapacityPrecise(gpa, n - 1) catch {};
5442 } else |_| {}
55 if (options.track_ids) {43 return pool;
56 try pool.ids.ensureTotalCapacity(gpa, 1 + thread_count);
57 pool.ids.putAssumeCapacityNoClobber(std.Thread.getCurrentId(), {});
58 }
59}44}
6045
61pub fn deinit(pool: *Pool) void {46pub fn deinit(pool: *Pool) void {
62 const gpa = pool.allocator;47 const gpa = pool.allocator;
63 pool.join();48 pool.join();
64 pool.threads.deinit(gpa);49 pool.threads.deinit(gpa);
65 pool.ids.deinit(gpa);
66 pool.* = undefined;50 pool.* = undefined;
67}51}
6852
69fn join(pool: *Pool) void {53fn join(pool: *Pool) void {
70 if (builtin.single_threaded) return;54 if (builtin.single_threaded) return;
71
72 {55 {
73 pool.mutex.lock();56 pool.mutex.lock();
74 defer pool.mutex.unlock();57 defer pool.mutex.unlock();
7558 pool.join_requested = true;
76 // ensure future worker threads exit the dequeue loop
77 pool.is_running = false;
78 }59 }
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.
82 pool.cond.broadcast();60 pool.cond.broadcast();
83 for (pool.threads.items) |thread| thread.join();61 for (pool.threads.items) |thread| thread.join();
84}62}
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
280fn worker(pool: *Pool) void {64fn worker(pool: *Pool) void {
281 pool.mutex.lock();65 pool.mutex.lock();
282 defer pool.mutex.unlock();66 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
287 while (true) {68 while (true) {
288 while (pool.run_queue.popFirst()) |run_node| {69 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());
313 pool.mutex.unlock();70 pool.mutex.unlock();
314 const runnable: *Runnable = @fieldParentPtr("node", run_node);71 const runnable: *Runnable = @fieldParentPtr("node", run_node);
315 runnable.runFn(runnable, id);72 runnable.start(runnable);
316 continue;73 pool.mutex.lock();
74 if (runnable.is_parallel) {
75 // TODO also pop thread and join sometimes
76 pool.parallel_count -= 1;
77 }
317 }78 }
31879 if (pool.join_requested) break;
319 pool.mutex.unlock();80 pool.cond.wait(&pool.mutex);
320 wait_group.wait();
321 return;
322 }81 }
323}82}
32483
325pub fn getIdCount(pool: *Pool) usize {
326 return @intCast(1 + pool.threads.items.len);
327}
328
329pub fn io(pool: *Pool) Io {84pub fn io(pool: *Pool) Io {
330 return .{85 return .{
331 .userdata = pool,86 .userdata = pool,
332 .vtable = &.{87 .vtable = &.{
333 .async = async,88 .async = async,
89 .asyncConcurrent = asyncParallel,
90 .asyncParallel = asyncParallel,
334 .await = await,91 .await = await,
335 .asyncDetached = asyncDetached,92 .asyncDetached = asyncDetached,
336 .cancel = cancel,93 .cancel = cancel,
...@@ -357,7 +114,7 @@ pub fn io(pool: *Pool) Io {...@@ -357,7 +114,7 @@ pub fn io(pool: *Pool) Io {
357114
358const AsyncClosure = struct {115const AsyncClosure = struct {
359 func: *const fn (context: *anyopaque, result: *anyopaque) void,116 func: *const fn (context: *anyopaque, result: *anyopaque) void,
360 runnable: Runnable = .{ .runFn = runFn },117 runnable: Runnable,
361 reset_event: std.Thread.ResetEvent,118 reset_event: std.Thread.ResetEvent,
362 select_condition: ?*std.Thread.ResetEvent,119 select_condition: ?*std.Thread.ResetEvent,
363 cancel_tid: std.Thread.Id,120 cancel_tid: std.Thread.Id,
...@@ -375,7 +132,7 @@ const AsyncClosure = struct {...@@ -375,7 +132,7 @@ const AsyncClosure = struct {
375 else => @compileError("unsupported std.Thread.Id: " ++ @typeName(std.Thread.Id)),132 else => @compileError("unsupported std.Thread.Id: " ++ @typeName(std.Thread.Id)),
376 };133 };
377134
378 fn runFn(runnable: *Pool.Runnable, _: ?usize) void {135 fn start(runnable: *Runnable) void {
379 const closure: *AsyncClosure = @alignCast(@fieldParentPtr("runnable", runnable));136 const closure: *AsyncClosure = @alignCast(@fieldParentPtr("runnable", runnable));
380 const tid = std.Thread.getCurrentId();137 const tid = std.Thread.getCurrentId();
381 if (@cmpxchgStrong(138 if (@cmpxchgStrong(
...@@ -387,6 +144,7 @@ const AsyncClosure = struct {...@@ -387,6 +144,7 @@ const AsyncClosure = struct {
387 .acquire,144 .acquire,
388 )) |cancel_tid| {145 )) |cancel_tid| {
389 assert(cancel_tid == canceling_tid);146 assert(cancel_tid == canceling_tid);
147 closure.reset_event.set();
390 return;148 return;
391 }149 }
392 current_closure = closure;150 current_closure = closure;
...@@ -438,9 +196,13 @@ const AsyncClosure = struct {...@@ -438,9 +196,13 @@ const AsyncClosure = struct {
438196
439 fn waitAndFree(closure: *AsyncClosure, gpa: Allocator, result: []u8) void {197 fn waitAndFree(closure: *AsyncClosure, gpa: Allocator, result: []u8) void {
440 closure.reset_event.wait();198 closure.reset_event.wait();
441 const base: [*]align(@alignOf(AsyncClosure)) u8 = @ptrCast(closure);
442 @memcpy(result, closure.resultPointer()[0..result.len]);199 @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]);
444 }206 }
445};207};
446208
...@@ -452,18 +214,26 @@ fn async(...@@ -452,18 +214,26 @@ fn async(
452 context_alignment: std.mem.Alignment,214 context_alignment: std.mem.Alignment,
453 start: *const fn (context: *const anyopaque, result: *anyopaque) void,215 start: *const fn (context: *const anyopaque, result: *anyopaque) void,
454) ?*Io.AnyFuture {216) ?*Io.AnyFuture {
217 if (builtin.single_threaded) {
218 start(context.ptr, result.ptr);
219 return null;
220 }
455 const pool: *Pool = @alignCast(@ptrCast(userdata));221 const pool: *Pool = @alignCast(@ptrCast(userdata));
456 pool.mutex.lock();222 const cpu_count = pool.cpu_count catch {
457223 return asyncParallel(userdata, result.len, result_alignment, context, context_alignment, start) orelse {
224 start(context.ptr, result.ptr);
225 return null;
226 };
227 };
458 const gpa = pool.allocator;228 const gpa = pool.allocator;
459 const context_offset = context_alignment.forward(@sizeOf(AsyncClosure));229 const context_offset = context_alignment.forward(@sizeOf(AsyncClosure));
460 const result_offset = result_alignment.forward(context_offset + context.len);230 const result_offset = result_alignment.forward(context_offset + context.len);
461 const n = result_offset + result.len;231 const n = result_offset + result.len;
462 const closure: *AsyncClosure = @alignCast(@ptrCast(gpa.alignedAlloc(u8, .of(AsyncClosure), n) catch {232 const closure: *AsyncClosure = @alignCast(@ptrCast(gpa.alignedAlloc(u8, .of(AsyncClosure), n) catch {
463 pool.mutex.unlock();
464 start(context.ptr, result.ptr);233 start(context.ptr, result.ptr);
465 return null;234 return null;
466 }));235 }));
236
467 closure.* = .{237 closure.* = .{
468 .func = start,238 .func = start,
469 .context_offset = context_offset,239 .context_offset = context_offset,
...@@ -471,37 +241,124 @@ fn async(...@@ -471,37 +241,124 @@ fn async(
471 .reset_event = .{},241 .reset_event = .{},
472 .cancel_tid = 0,242 .cancel_tid = 0,
473 .select_condition = null,243 .select_condition = null,
244 .runnable = .{
245 .start = AsyncClosure.start,
246 .is_parallel = false,
247 },
474 };248 };
249
475 @memcpy(closure.contextPointer()[0..context.len], context);250 @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
476 pool.run_queue.prepend(&closure.runnable.node);263 pool.run_queue.prepend(&closure.runnable.node);
477264
478 if (pool.threads.items.len < pool.threads.capacity) {265 if (pool.threads.items.len < thread_capacity) {
479 pool.threads.addOneAssumeCapacity().* = std.Thread.spawn(.{266 const thread = std.Thread.spawn(.{ .stack_size = pool.stack_size }, worker, .{pool}) catch {
480 .stack_size = pool.stack_size,267 if (pool.threads.items.len == 0) {
481 .allocator = gpa,268 assert(pool.run_queue.popFirst() == &closure.runnable.node);
482 }, worker, .{pool}) catch t: {269 pool.mutex.unlock();
483 pool.threads.items.len -= 1;270 closure.free(gpa, result.len);
484 break :t undefined;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);
485 };278 };
279 pool.threads.appendAssumeCapacity(thread);
486 }280 }
487281
488 pool.mutex.unlock();282 pool.mutex.unlock();
489 pool.cond.signal();283 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();
491 return @ptrCast(closure);344 return @ptrCast(closure);
492}345}
493346
494const DetachedClosure = struct {347const DetachedClosure = struct {
495 pool: *Pool,348 pool: *Pool,
496 func: *const fn (context: *anyopaque) void,349 func: *const fn (context: *anyopaque) void,
497 runnable: Runnable = .{ .runFn = runFn },350 runnable: Runnable,
498 context_alignment: std.mem.Alignment,351 context_alignment: std.mem.Alignment,
499 context_len: usize,352 context_len: usize,
500353
501 fn runFn(runnable: *Pool.Runnable, _: ?usize) void {354 fn start(runnable: *Runnable) void {
502 const closure: *DetachedClosure = @alignCast(@fieldParentPtr("runnable", runnable));355 const closure: *DetachedClosure = @alignCast(@fieldParentPtr("runnable", runnable));
503 closure.func(closure.contextPointer());356 closure.func(closure.contextPointer());
504 const gpa = closure.pool.allocator;357 const gpa = closure.pool.allocator;
358 free(closure, gpa);
359 }
360
361 fn free(closure: *DetachedClosure, gpa: Allocator) void {
505 const base: [*]align(@alignOf(DetachedClosure)) u8 = @ptrCast(closure);362 const base: [*]align(@alignOf(DetachedClosure)) u8 = @ptrCast(closure);
506 gpa.free(base[0..contextEnd(closure.context_alignment, closure.context_len)]);363 gpa.free(base[0..contextEnd(closure.context_alignment, closure.context_len)]);
507 }364 }
...@@ -526,33 +383,46 @@ fn asyncDetached(...@@ -526,33 +383,46 @@ fn asyncDetached(
526 context_alignment: std.mem.Alignment,383 context_alignment: std.mem.Alignment,
527 start: *const fn (context: *const anyopaque) void,384 start: *const fn (context: *const anyopaque) void,
528) void {385) void {
386 if (builtin.single_threaded) return start(context.ptr);
529 const pool: *Pool = @alignCast(@ptrCast(userdata));387 const pool: *Pool = @alignCast(@ptrCast(userdata));
530 pool.mutex.lock();388 const cpu_count = pool.cpu_count catch 1;
531
532 const gpa = pool.allocator;389 const gpa = pool.allocator;
533 const n = DetachedClosure.contextEnd(context_alignment, context.len);390 const n = DetachedClosure.contextEnd(context_alignment, context.len);
534 const closure: *DetachedClosure = @alignCast(@ptrCast(gpa.alignedAlloc(u8, .of(DetachedClosure), n) catch {391 const closure: *DetachedClosure = @alignCast(@ptrCast(gpa.alignedAlloc(u8, .of(DetachedClosure), n) catch {
535 pool.mutex.unlock();392 return start(context.ptr);
536 start(context.ptr);
537 return;
538 }));393 }));
539 closure.* = .{394 closure.* = .{
540 .pool = pool,395 .pool = pool,
541 .func = start,396 .func = start,
542 .context_alignment = context_alignment,397 .context_alignment = context_alignment,
543 .context_len = context.len,398 .context_len = context.len,
399 .runnable = .{
400 .start = DetachedClosure.start,
401 .is_parallel = false,
402 },
544 };403 };
545 @memcpy(closure.contextPointer()[0..context.len], context);404 @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
546 pool.run_queue.prepend(&closure.runnable.node);416 pool.run_queue.prepend(&closure.runnable.node);
547417
548 if (pool.threads.items.len < pool.threads.capacity) {418 if (pool.threads.items.len < thread_capacity) {
549 pool.threads.addOneAssumeCapacity().* = std.Thread.spawn(.{419 const thread = std.Thread.spawn(.{ .stack_size = pool.stack_size }, worker, .{pool}) catch {
550 .stack_size = pool.stack_size,420 assert(pool.run_queue.popFirst() == &closure.runnable.node);
551 .allocator = gpa,421 pool.mutex.unlock();
552 }, worker, .{pool}) catch t: {422 closure.free(gpa);
553 pool.threads.items.len -= 1;423 return start(context.ptr);
554 break :t undefined;
555 };424 };
425 pool.threads.appendAssumeCapacity(thread);
556 }426 }
557427
558 pool.mutex.unlock();428 pool.mutex.unlock();
lib/std/Thread.zig+2
...@@ -384,6 +384,8 @@ pub const CpuCountError = error{...@@ -384,6 +384,8 @@ pub const CpuCountError = error{
384};384};
385385
386/// Returns the platforms view on the number of logical CPU cores available.386/// Returns the platforms view on the number of logical CPU cores available.
387///
388/// Returned value guaranteed to be >= 1.
387pub fn getCpuCount() CpuCountError!usize {389pub fn getCpuCount() CpuCountError!usize {
388 return try Impl.getCpuCount();390 return try Impl.getCpuCount();
389}391}