| author | |
| committer | |
| log | 32dc46aae56623bff9b1fc792d49913f9295be7b |
| tree | 5a651c680c3f845c9365a39164f5339a08fea4bc |
| parent | 476d7d939cf7cc7b336d8f3b8ff1dad429cf343e |
A function that participates in a group but guarantees allocation of one
unit of concurrency, or returns an error.3 files changed, 123 insertions(+), 3 deletions(-)
lib/std/Io.zig+43-3| ... | @@ -626,8 +626,9 @@ pub const VTable = struct { | ... | @@ -626,8 +626,9 @@ pub const VTable = struct { |
| 626 | /// Thread-safe. | 626 | /// Thread-safe. |
| 627 | cancelRequested: *const fn (?*anyopaque) bool, | 627 | cancelRequested: *const fn (?*anyopaque) bool, |
| 628 | 628 | ||
| 629 | /// Executes `start` asynchronously in a manner such that it cleans itself | 629 | /// When this function returns, implementation guarantees that `start` has |
| 630 | /// up. This mode does not support results, await, or cancel. | 630 | /// either already been called, or a unit of concurrency has been assigned |
| 631 | /// to the task of calling the function. | ||
| 631 | /// | 632 | /// |
| 632 | /// Thread-safe. | 633 | /// Thread-safe. |
| 633 | groupAsync: *const fn ( | 634 | groupAsync: *const fn ( |
| ... | @@ -640,6 +641,17 @@ pub const VTable = struct { | ... | @@ -640,6 +641,17 @@ pub const VTable = struct { |
| 640 | context_alignment: std.mem.Alignment, | 641 | context_alignment: std.mem.Alignment, |
| 641 | start: *const fn (*Group, context: *const anyopaque) void, | 642 | start: *const fn (*Group, context: *const anyopaque) void, |
| 642 | ) void, | 643 | ) void, |
| 644 | /// Thread-safe. | ||
| 645 | groupConcurrent: *const fn ( | ||
| 646 | /// Corresponds to `Io.userdata`. | ||
| 647 | userdata: ?*anyopaque, | ||
| 648 | /// Owner of the spawned async task. | ||
| 649 | group: *Group, | ||
| 650 | /// Copied and then passed to `start`. | ||
| 651 | context: []const u8, | ||
| 652 | context_alignment: std.mem.Alignment, | ||
| 653 | start: *const fn (*Group, context: *const anyopaque) void, | ||
| 654 | ) ConcurrentError!void, | ||
| 643 | groupWait: *const fn (?*anyopaque, *Group, token: *anyopaque) void, | 655 | groupWait: *const fn (?*anyopaque, *Group, token: *anyopaque) void, |
| 644 | groupCancel: *const fn (?*anyopaque, *Group, token: *anyopaque) void, | 656 | groupCancel: *const fn (?*anyopaque, *Group, token: *anyopaque) void, |
| 645 | 657 | ||
| ... | @@ -1021,8 +1033,8 @@ pub const Group = struct { | ... | @@ -1021,8 +1033,8 @@ pub const Group = struct { |
| 1021 | /// Threadsafe. | 1033 | /// Threadsafe. |
| 1022 | /// | 1034 | /// |
| 1023 | /// See also: | 1035 | /// See also: |
| 1024 | /// * `Io.async` | ||
| 1025 | /// * `concurrent` | 1036 | /// * `concurrent` |
| 1037 | /// * `Io.async` | ||
| 1026 | pub fn async(g: *Group, io: Io, function: anytype, args: std.meta.ArgsTuple(@TypeOf(function))) void { | 1038 | pub fn async(g: *Group, io: Io, function: anytype, args: std.meta.ArgsTuple(@TypeOf(function))) void { |
| 1027 | const Args = @TypeOf(args); | 1039 | const Args = @TypeOf(args); |
| 1028 | const TypeErased = struct { | 1040 | const TypeErased = struct { |
| ... | @@ -1035,6 +1047,34 @@ pub const Group = struct { | ... | @@ -1035,6 +1047,34 @@ pub const Group = struct { |
| 1035 | io.vtable.groupAsync(io.userdata, g, @ptrCast(&args), .of(Args), TypeErased.start); | 1047 | io.vtable.groupAsync(io.userdata, g, @ptrCast(&args), .of(Args), TypeErased.start); |
| 1036 | } | 1048 | } |
| 1037 | 1049 | ||
| 1050 | /// Calls `function` with `args`, such that the function is not guaranteed | ||
| 1051 | /// to have returned until `wait` is called, allowing the caller to | ||
| 1052 | /// progress while waiting for any `Io` operations. | ||
| 1053 | /// | ||
| 1054 | /// The resource spawned is owned by the group; after this is called, | ||
| 1055 | /// `wait` or `cancel` must be called before the group is deinitialized. | ||
| 1056 | /// | ||
| 1057 | /// This has stronger guarantee than `async`, placing restrictions on what kind | ||
| 1058 | /// of `Io` implementations are supported. By calling `async` instead, one | ||
| 1059 | /// allows, for example, stackful single-threaded blocking I/O. | ||
| 1060 | /// | ||
| 1061 | /// Threadsafe. | ||
| 1062 | /// | ||
| 1063 | /// See also: | ||
| 1064 | /// * `async` | ||
| 1065 | /// * `Io.concurrent` | ||
| 1066 | pub fn concurrent(g: *Group, io: Io, function: anytype, args: std.meta.ArgsTuple(@TypeOf(function))) ConcurrentError!void { | ||
| 1067 | const Args = @TypeOf(args); | ||
| 1068 | const TypeErased = struct { | ||
| 1069 | fn start(group: *Group, context: *const anyopaque) void { | ||
| 1070 | _ = group; | ||
| 1071 | const args_casted: *const Args = @ptrCast(@alignCast(context)); | ||
| 1072 | @call(.auto, function, args_casted.*); | ||
| 1073 | } | ||
| 1074 | }; | ||
| 1075 | return io.vtable.groupConcurrent(io.userdata, g, @ptrCast(&args), .of(Args), TypeErased.start); | ||
| 1076 | } | ||
| 1077 | |||
| 1038 | /// Blocks until all tasks of the group finish. During this time, | 1078 | /// Blocks until all tasks of the group finish. During this time, |
| 1039 | /// cancellation requests propagate to all members of the group. | 1079 | /// cancellation requests propagate to all members of the group. |
| 1040 | /// | 1080 | /// |
lib/std/Io/Threaded.zig+54| ... | @@ -116,6 +116,7 @@ pub fn init( | ... | @@ -116,6 +116,7 @@ pub fn init( |
| 116 | /// * `Io.VTable.async` | 116 | /// * `Io.VTable.async` |
| 117 | /// * `Io.VTable.concurrent` | 117 | /// * `Io.VTable.concurrent` |
| 118 | /// * `Io.VTable.groupAsync` | 118 | /// * `Io.VTable.groupAsync` |
| 119 | /// * `Io.VTable.groupConcurrent` | ||
| 119 | /// If these functions are avoided, then `Allocator.failing` may be passed | 120 | /// If these functions are avoided, then `Allocator.failing` may be passed |
| 120 | /// here. | 121 | /// here. |
| 121 | gpa: Allocator, | 122 | gpa: Allocator, |
| ... | @@ -221,6 +222,7 @@ pub fn io(t: *Threaded) Io { | ... | @@ -221,6 +222,7 @@ pub fn io(t: *Threaded) Io { |
| 221 | .select = select, | 222 | .select = select, |
| 222 | 223 | ||
| 223 | .groupAsync = groupAsync, | 224 | .groupAsync = groupAsync, |
| 225 | .groupConcurrent = groupConcurrent, | ||
| 224 | .groupWait = groupWait, | 226 | .groupWait = groupWait, |
| 225 | .groupCancel = groupCancel, | 227 | .groupCancel = groupCancel, |
| 226 | 228 | ||
| ... | @@ -317,6 +319,7 @@ pub fn ioBasic(t: *Threaded) Io { | ... | @@ -317,6 +319,7 @@ pub fn ioBasic(t: *Threaded) Io { |
| 317 | .select = select, | 319 | .select = select, |
| 318 | 320 | ||
| 319 | .groupAsync = groupAsync, | 321 | .groupAsync = groupAsync, |
| 322 | .groupConcurrent = groupConcurrent, | ||
| 320 | .groupWait = groupWait, | 323 | .groupWait = groupWait, |
| 321 | .groupCancel = groupCancel, | 324 | .groupCancel = groupCancel, |
| 322 | 325 | ||
| ... | @@ -729,6 +732,57 @@ fn groupAsync( | ... | @@ -729,6 +732,57 @@ fn groupAsync( |
| 729 | t.cond.signal(); | 732 | t.cond.signal(); |
| 730 | } | 733 | } |
| 731 | 734 | ||
| 735 | fn groupConcurrent( | ||
| 736 | userdata: ?*anyopaque, | ||
| 737 | group: *Io.Group, | ||
| 738 | context: []const u8, | ||
| 739 | context_alignment: Alignment, | ||
| 740 | start: *const fn (*Io.Group, context: *const anyopaque) void, | ||
| 741 | ) Io.ConcurrentError!void { | ||
| 742 | if (builtin.single_threaded) return error.ConcurrencyUnavailable; | ||
| 743 | |||
| 744 | const t: *Threaded = @ptrCast(@alignCast(userdata)); | ||
| 745 | |||
| 746 | const gpa = t.allocator; | ||
| 747 | const gc = GroupClosure.init(gpa, t, group, context, context_alignment, start) catch | ||
| 748 | return error.ConcurrencyUnavailable; | ||
| 749 | |||
| 750 | t.mutex.lock(); | ||
| 751 | defer t.mutex.unlock(); | ||
| 752 | |||
| 753 | const busy_count = t.busy_count; | ||
| 754 | |||
| 755 | if (busy_count >= @intFromEnum(t.concurrent_limit)) | ||
| 756 | return error.ConcurrencyUnavailable; | ||
| 757 | |||
| 758 | t.busy_count = busy_count + 1; | ||
| 759 | errdefer t.busy_count = busy_count; | ||
| 760 | |||
| 761 | const pool_size = t.wait_group.value(); | ||
| 762 | if (pool_size - busy_count == 0) { | ||
| 763 | t.wait_group.start(); | ||
| 764 | errdefer t.wait_group.finish(); | ||
| 765 | |||
| 766 | const thread = std.Thread.spawn(.{ .stack_size = t.stack_size }, worker, .{t}) catch | ||
| 767 | return error.ConcurrencyUnavailable; | ||
| 768 | thread.detach(); | ||
| 769 | } | ||
| 770 | |||
| 771 | // Append to the group linked list inside the mutex to make `Io.Group.concurrent` thread-safe. | ||
| 772 | gc.node = .{ .next = @ptrCast(@alignCast(group.token)) }; | ||
| 773 | group.token = &gc.node; | ||
| 774 | |||
| 775 | t.run_queue.prepend(&gc.closure.node); | ||
| 776 | |||
| 777 | // This needs to be done before unlocking the mutex to avoid a race with | ||
| 778 | // the associated task finishing. | ||
| 779 | const group_state: *std.atomic.Value(usize) = @ptrCast(&group.state); | ||
| 780 | const prev_state = group_state.fetchAdd(GroupClosure.sync_one_pending, .monotonic); | ||
| 781 | assert((prev_state / GroupClosure.sync_one_pending) < (std.math.maxInt(usize) / GroupClosure.sync_one_pending)); | ||
| 782 | |||
| 783 | t.cond.signal(); | ||
| 784 | } | ||
| 785 | |||
| 732 | fn groupWait(userdata: ?*anyopaque, group: *Io.Group, token: *anyopaque) void { | 786 | fn groupWait(userdata: ?*anyopaque, group: *Io.Group, token: *anyopaque) void { |
| 733 | const t: *Threaded = @ptrCast(@alignCast(userdata)); | 787 | const t: *Threaded = @ptrCast(@alignCast(userdata)); |
| 734 | const gpa = t.allocator; | 788 | const gpa = t.allocator; |
lib/std/Io/test.zig+26| ... | @@ -172,6 +172,32 @@ fn sleep(io: Io, result: *usize) void { | ... | @@ -172,6 +172,32 @@ fn sleep(io: Io, result: *usize) void { |
| 172 | result.* = 1; | 172 | result.* = 1; |
| 173 | } | 173 | } |
| 174 | 174 | ||
| 175 | test "Group concurrent" { | ||
| 176 | const io = testing.io; | ||
| 177 | |||
| 178 | var group: Io.Group = .init; | ||
| 179 | defer group.cancel(io); | ||
| 180 | var results: [2]usize = undefined; | ||
| 181 | |||
| 182 | group.concurrent(io, count, .{ 1, 10, &results[0] }) catch |err| switch (err) { | ||
| 183 | error.ConcurrencyUnavailable => { | ||
| 184 | try testing.expect(builtin.single_threaded); | ||
| 185 | return; | ||
| 186 | }, | ||
| 187 | }; | ||
| 188 | |||
| 189 | group.concurrent(io, count, .{ 20, 30, &results[1] }) catch |err| switch (err) { | ||
| 190 | error.ConcurrencyUnavailable => { | ||
| 191 | try testing.expect(builtin.single_threaded); | ||
| 192 | return; | ||
| 193 | }, | ||
| 194 | }; | ||
| 195 | |||
| 196 | group.wait(io); | ||
| 197 | |||
| 198 | try testing.expectEqualSlices(usize, &.{ 45, 245 }, &results); | ||
| 199 | } | ||
| 200 | |||
| 175 | test "select" { | 201 | test "select" { |
| 176 | const io = testing.io; | 202 | const io = testing.io; |
| 177 | 203 |