├── src ├── main.zig ├── channel.zig └── task.zig ├── .gitignore ├── LICENSE ├── examples ├── test.zig └── test2.zig └── README.md /src/main.zig: -------------------------------------------------------------------------------- 1 | pub const Future = @import("task.zig").Future; 2 | pub const Task = @import("task.zig").Task; 3 | pub const Channel = @import("channel.zig").Channel; 4 | 5 | test "main test" { 6 | _ = @import("task.zig"); 7 | _ = @import("channel.zig"); 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Object files 5 | *.o 6 | *.ko 7 | *.obj 8 | *.elf 9 | 10 | # Linker output 11 | *.ilk 12 | *.map 13 | *.exp 14 | 15 | # Precompiled Headers 16 | *.gch 17 | *.pch 18 | 19 | # Libraries 20 | *.lib 21 | *.a 22 | *.la 23 | *.lo 24 | 25 | # Shared objects (inc. Windows DLLs) 26 | *.dll 27 | *.so 28 | *.so.* 29 | *.dylib 30 | 31 | # Executables 32 | *.exe 33 | *.out 34 | *.app 35 | *.i*86 36 | *.x86_64 37 | *.hex 38 | 39 | # Debug files 40 | *.dSYM/ 41 | *.su 42 | *.idb 43 | *.pdb 44 | 45 | # Kernel Module Compile Results 46 | *.mod* 47 | *.cmd 48 | .tmp_versions/ 49 | modules.order 50 | Module.symvers 51 | Mkfile.old 52 | dkms.conf 53 | 54 | # editor temp file 55 | *.swp 56 | 57 | # ziglang temp files 58 | zig-cache 59 | zig-out 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 jack 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/test.zig: -------------------------------------------------------------------------------- 1 | 2 | // task example 3 | 4 | const std = @import("std"); 5 | const print = std.debug.print; 6 | 7 | //const Future = @import("zig_async_jack_ji/task.zig").Future; 8 | const Task = @import("zig_async_jack_ji/task.zig").Task; 9 | //const Channel = @import("zig_async_jack_ji/channel.zig").Channel; 10 | 11 | 12 | fn randomInteger(rand: *const std.Random, min: u32, max: u32) u32{ 13 | return rand.intRangeLessThan(u32, min, max + 1); 14 | } 15 | 16 | fn make_square(number: u32) void{ 17 | var seed: u64 = undefined; 18 | std.crypto.random.bytes(std.mem.asBytes(&seed)); 19 | var prng = std.Random.DefaultPrng.init(seed); 20 | const rand = prng.random(); 21 | const rand_delay = randomInteger(&rand, 1, 5); 22 | std.time.sleep(rand_delay * std.time.ns_per_s); 23 | 24 | const result = number * number; 25 | std.debug.print("{d} -> {d}\n", .{ number, result }); 26 | } 27 | 28 | 29 | pub fn main() !void{ 30 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 31 | defer _ = gpa.deinit(); 32 | const allocator = gpa.allocator(); 33 | 34 | const numbers = [_]u32{ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }; 35 | 36 | var futures: [numbers.len]*Task(make_square).FutureType = undefined; 37 | 38 | // launch all tasks 39 | for(numbers, 0..) |num, i|{ 40 | futures[i] = try Task(make_square).launch(allocator, .{num}); 41 | } 42 | 43 | // wait for all to complete 44 | for(futures) |future|{ 45 | future.wait(); 46 | future.deinit(); 47 | } 48 | 49 | std.debug.print("All tasks completed.\n", .{}); 50 | } 51 | 52 | 53 | // zig build-exe ./src/test.zig -O ReleaseFast -femit-bin=test 54 | // ./test 55 | //10 -> 100 56 | //50 -> 2500 57 | //20 -> 400 58 | //80 -> 6400 59 | //30 -> 900 60 | //70 -> 4900 61 | //60 -> 3600 62 | //100 -> 10000 63 | //40 -> 1600 64 | //90 -> 8100 65 | //All tasks completed. 66 | 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zig-async 2 | An simple and easy to use async task library for zig. 3 | 4 | ## Async Task 5 | Task running in separate thread, returns `Future` after launched. 6 | `Future` represents task's return value in the future, which can be queried by using its waiting methods. 7 | The wrapped data within `Future` will be automatically destroyed if supported by struct (has `deinit` method); 8 | 9 | ```zig 10 | const Result = struct { 11 | const Self = @This(); 12 | 13 | allocator: std.mem.Allocator, 14 | c: u32, 15 | 16 | pub fn init(allocator: std.mem.Allocator, _c: u32) !*Self { 17 | var self = try allocator.create(Self); 18 | self.* = .{ 19 | .allocator = allocator, 20 | .c = _c, 21 | }; 22 | return self; 23 | } 24 | 25 | /// Will be called automatically when destorying future 26 | pub fn deinit(self: *Self) void { 27 | self.allocator.destroy(self); 28 | } 29 | }; 30 | 31 | fn add(a: u32, b: u32) !*Result { 32 | return try Result.init(std.testing.allocator, a + b); 33 | } 34 | 35 | const MyTask = Task(add); 36 | var future = try MyTask.launch(std.testing.allocator, .{ 2, 1 }); 37 | defer future.deinit(); 38 | const ret = future.wait(); 39 | try testing.expectEqual(@as(u32, 3), if (ret) |d| d.c else |_| unreachable); 40 | ``` 41 | 42 | ## Channel 43 | Generic message queue used for communicating between threads. 44 | Capable of free memory automatically if supported by embedded struct (has `deinit` method). 45 | 46 | ```zig 47 | const MyData = struct { 48 | d: i32, 49 | 50 | /// Will be called automatically when destorying popped result 51 | pub fn deinit(self: *@This()) void { 52 | } 53 | }; 54 | 55 | const MyChannel = Channel(MyData); 56 | var channel = try MyChannel.init(std.testing.allocator); 57 | defer channel.deinit(); 58 | 59 | try channel.push(.{ .d = 1 }); 60 | try channel.push(.{ .d = 2 }); 61 | try channel.push(.{ .d = 3 }); 62 | try channel.push(.{ .d = 4 }); 63 | try channel.push(.{ .d = 5 }); 64 | 65 | try testing.expect(channel.pop().?.d, 1); 66 | 67 | var result = channel.popn(3).?; 68 | defer result.deinit(); 69 | try testing.expect(result.elements[0].d == 2); 70 | try testing.expect(result.elements[1].d == 3); 71 | try testing.expect(result.elements[2].d == 4); 72 | ``` 73 | -------------------------------------------------------------------------------- /examples/test2.zig: -------------------------------------------------------------------------------- 1 | 2 | // channel example 3 | // producer - worker - consumer model via channels 4 | 5 | const std = @import("std"); 6 | const print = std.debug.print; 7 | 8 | //const Future = @import("zig_async_jack_ji/task.zig").Future; 9 | const Task = @import("zig_async_jack_ji/task.zig").Task; 10 | const Channel = @import("zig_async_jack_ji/channel.zig").Channel; 11 | 12 | 13 | //fn randomInteger(rand: *const std.Random, min: u32, max: u32) u32{ 14 | // return rand.intRangeLessThan(u32, min, max + 1); 15 | //} 16 | 17 | 18 | fn worker(input_ch: *Channel(u32), output_ch: *Channel(u32)) void{ 19 | while(true){ 20 | const maybe_num = input_ch.pop(); 21 | if(maybe_num) |num|{ 22 | if(num == 0){ break; } // sentinel to stop 23 | const cube = num * num * num; 24 | output_ch.push(cube) catch unreachable; 25 | }else{ 26 | std.time.sleep(std.time.ns_per_ms * 10); // avoid busy wait 27 | } 28 | 29 | //var seed: u64 = undefined; 30 | //std.crypto.random.bytes(std.mem.asBytes(&seed)); 31 | //var prng = std.Random.DefaultPrng.init(seed); 32 | //const rand = prng.random(); 33 | //const rand_delay = randomInteger(&rand, 1, 5); 34 | //std.time.sleep(rand_delay * std.time.ns_per_s); 35 | 36 | } 37 | } 38 | 39 | 40 | pub fn main() !void{ 41 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 42 | defer _ = gpa.deinit(); 43 | const allocator = gpa.allocator(); 44 | 45 | var input_ch = try Channel(u32).init(allocator); 46 | defer input_ch.deinit(); 47 | 48 | var output_ch = try Channel(u32).init(allocator); 49 | defer output_ch.deinit(); 50 | 51 | // launch worker thread 52 | const WorkerTask = Task(worker); 53 | var worker_future = try WorkerTask.launch(allocator, .{ input_ch, output_ch }); 54 | defer worker_future.deinit(); 55 | 56 | const inputs = [_]u32{ 2, 3, 4, 5, 6 }; 57 | 58 | // send data to worker 59 | for(inputs) |num|{ 60 | input_ch.push(num) catch unreachable; 61 | } 62 | 63 | // send sentinel to stop worker 64 | input_ch.push(0) catch unreachable; 65 | 66 | // collect results 67 | var received: usize = 0; 68 | while(received < inputs.len) : (received += 1){ 69 | while(true){ 70 | if(output_ch.pop()) |result|{ 71 | std.debug.print("Receive {d}\n", .{result}); 72 | break; 73 | } 74 | std.time.sleep(std.time.ns_per_ms * 5); 75 | } 76 | } 77 | 78 | worker_future.wait(); 79 | std.debug.print("All done.\n", .{}); 80 | } 81 | 82 | 83 | // zig build-exe ./src/test2.zig -O ReleaseFast -femit-bin=test2 84 | // ./test2 85 | //Receive 8 86 | //Receive 27 87 | //Receive 64 88 | //Receive 125 89 | //Receive 216 90 | //All done. 91 | 92 | -------------------------------------------------------------------------------- /src/channel.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const testing = std.testing; 3 | const trait = std.meta; 4 | 5 | /// Communication channel between threads 6 | pub fn Channel(comptime T: type) type { 7 | return struct { 8 | const Self = @This(); 9 | const Deque = std.fifo.LinearFifo(T, .Dynamic); 10 | 11 | allocator: std.mem.Allocator, 12 | mutex: std.Thread.Mutex, 13 | fifo: Deque, 14 | 15 | pub fn init(allocator: std.mem.Allocator) !*Self { 16 | const self = try allocator.create(Self); 17 | self.* = .{ 18 | .allocator = allocator, 19 | .mutex = std.Thread.Mutex{}, 20 | .fifo = Deque.init(allocator), 21 | }; 22 | return self; 23 | } 24 | 25 | pub fn deinit(self: *Self) void { 26 | while (self.fifo.readItem()) |elem| { 27 | if (comptime trait.hasFn(T, "deinit")) { 28 | elem.deinit(); // Destroy data when possible 29 | } 30 | } 31 | self.fifo.deinit(); 32 | self.allocator.destroy(self); 33 | } 34 | 35 | /// Push data to channel 36 | pub fn push(self: *Self, data: T) !void { 37 | self.mutex.lock(); 38 | defer self.mutex.unlock(); 39 | try self.fifo.writeItem(data); 40 | } 41 | 42 | /// Popped data from channel 43 | pub const PopResult = struct { 44 | allocator: std.mem.Allocator, 45 | elements: std.ArrayList(T), 46 | 47 | pub fn deinit(self: PopResult) void { 48 | for (self.elements.items) |*data| { 49 | if (comptime trait.hasFn(T, "deinit")) { 50 | data.deinit(); // Destroy data when possible 51 | } 52 | } 53 | self.elements.deinit(); 54 | } 55 | }; 56 | 57 | /// Get data from channel, data will be destroyed together with PopResult 58 | pub fn popn(self: *Self, max_pop: usize) ?PopResult { 59 | self.mutex.lock(); 60 | defer self.mutex.unlock(); 61 | var result = PopResult{ 62 | .allocator = self.allocator, 63 | .elements = std.ArrayList(T).init(self.allocator), 64 | }; 65 | var count = max_pop; 66 | while (count > 0) : (count -= 1) { 67 | if (self.fifo.readItem()) |data| { 68 | result.elements.append(data) catch unreachable; 69 | } else { 70 | break; 71 | } 72 | } 73 | return if (count == max_pop) null else result; 74 | } 75 | 76 | /// Get data from channel, user take ownership 77 | pub fn pop(self: *Self) ?T { 78 | self.mutex.lock(); 79 | defer self.mutex.unlock(); 80 | return self.fifo.readItem(); 81 | } 82 | }; 83 | } 84 | 85 | test "Channel - smoke testing" { 86 | const MyData = struct { 87 | d: i32, 88 | 89 | pub fn deinit(self: @This()) void { 90 | std.debug.print("\ndeinit mydata, d is {d} ", .{self.d}); 91 | } 92 | }; 93 | 94 | const MyChannel = Channel(MyData); 95 | var channel = try MyChannel.init(std.testing.allocator); 96 | defer channel.deinit(); 97 | 98 | try channel.push(.{ .d = 1 }); 99 | try channel.push(.{ .d = 2 }); 100 | try channel.push(.{ .d = 3 }); 101 | try channel.push(.{ .d = 4 }); 102 | try channel.push(.{ .d = 5 }); 103 | 104 | try testing.expect(channel.pop().?.d == 1); 105 | var result = channel.popn(3).?; 106 | defer result.deinit(); 107 | try testing.expect(result.elements.items[0].d == 2); 108 | try testing.expect(result.elements.items[1].d == 3); 109 | try testing.expect(result.elements.items[2].d == 4); 110 | } 111 | -------------------------------------------------------------------------------- /src/task.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const testing = std.testing; 3 | const trait = std.meta; 4 | const builtin = @import("builtin"); 5 | 6 | /// Represent a value returned by async task in the future. 7 | pub fn Future(comptime T: type) type { 8 | return struct { 9 | const Self = @This(); 10 | 11 | allocator: std.mem.Allocator, 12 | done: std.Thread.ResetEvent, 13 | data: ?T, 14 | 15 | pub fn init(allocator: std.mem.Allocator) !*Self { 16 | const self = try allocator.create(Self); 17 | self.* = .{ 18 | .allocator = allocator, 19 | .done = .{}, 20 | .data = null, 21 | }; 22 | return self; 23 | } 24 | 25 | pub fn deinit(self: *Self) void { 26 | if (!self.done.isSet()) { 27 | @panic("future isn't done yet!"); 28 | } 29 | if (self.data) |data| { 30 | // Destroy data when possible 31 | // Only detect one layer of optional/error-union 32 | switch (@typeInfo(T)) { 33 | .optional => |info| { 34 | if (comptime isSingleItemPtr(info.child) and 35 | trait.hasFn(@typeInfo(info.child), "deinit")) 36 | { 37 | if (data) |d| d.deinit(); 38 | } else if (comptime trait.hasFn(info.child, "deinit")) { 39 | if (data) |d| d.deinit(); 40 | } 41 | }, 42 | .error_union => |info| { 43 | if (comptime isSingleItemPtr(info.payload) and 44 | trait.hasFn(@typeInfo(info.payload).Pointer.child, "deinit")) 45 | { 46 | if (data) |d| d.deinit() else |_| {} 47 | } else if (comptime trait.hasFn(info.payload, "deinit")) { 48 | if (data) |d| d.deinit() else |_| {} 49 | } 50 | }, 51 | else => { 52 | if (comptime trait.hasFn(T, "deinit")) { 53 | data.deinit(); 54 | } 55 | }, 56 | } 57 | } 58 | self.allocator.destroy(self); 59 | } 60 | 61 | /// Wait until data is granted 62 | pub fn wait(self: *Self) T { 63 | self.done.wait(); 64 | std.debug.assert(self.data != null); 65 | return self.data.?; 66 | } 67 | 68 | /// Wait until data is granted or timeout happens 69 | pub fn timedWait(self: *Self, time_ns: u64) ?T { 70 | self.done.timedWait(time_ns) catch {}; 71 | return self.data; 72 | } 73 | 74 | /// Grant data and send signal to waiting threads 75 | pub fn grant(self: *Self, data: T) void { 76 | self.data = data; 77 | self.done.set(); 78 | } 79 | }; 80 | } 81 | 82 | /// Async task runs in another thread 83 | pub fn Task(comptime fun: anytype) type { 84 | return struct { 85 | pub const FunType = @TypeOf(fun); 86 | pub const ArgsType = std.meta.ArgsTuple(FunType); 87 | pub const ReturnType = @typeInfo(FunType).@"fn".return_type.?; 88 | pub const FutureType = Future(ReturnType); 89 | 90 | /// Internal thread function, run user's function and 91 | /// grant result to future. 92 | fn task(future: *FutureType, args: ArgsType) void { 93 | const ret = @call(.auto, fun, args); 94 | future.grant(ret); 95 | } 96 | 97 | /// Create task thread and detach from it 98 | pub fn launch(allocator: std.mem.Allocator, args: ArgsType) !*FutureType { 99 | var future = try FutureType.init(allocator); 100 | errdefer future.deinit(); 101 | var thread = try std.Thread.spawn(.{}, task, .{ future, args }); 102 | thread.detach(); 103 | return future; 104 | } 105 | }; 106 | } 107 | 108 | pub fn isSingleItemPtr(comptime T: type) bool { 109 | if (comptime @typeInfo(T) == .Pointer) { 110 | return @typeInfo(T).Pointer.size == .One; 111 | } 112 | return false; 113 | } 114 | 115 | test "Async Task" { 116 | if (builtin.single_threaded) return error.SkipZigTest; 117 | 118 | const channel = @import("channel.zig"); 119 | const Channel = channel.Channel; 120 | const S = struct { 121 | const R = struct { 122 | allocator: std.mem.Allocator, 123 | v: u32, 124 | 125 | pub fn deinit(self: *@This()) void { 126 | std.debug.print("\ndeinit R, v is {d}", .{self.v}); 127 | self.allocator.destroy(self); 128 | } 129 | }; 130 | 131 | fn div(allocator: std.mem.Allocator, a: u32, b: u32) !*R { 132 | if (b == 0) return error.DivisionByZero; 133 | const r = try allocator.create(R); 134 | r.* = .{ 135 | .allocator = allocator, 136 | .v = @divTrunc(a, b), 137 | }; 138 | return r; 139 | } 140 | 141 | fn return_nothing() void {} 142 | 143 | fn long_work(ch: *Channel(u32), a: u32, b: u32) u32 { 144 | std.time.sleep(std.time.ns_per_s); 145 | ch.push(std.math.pow(u32, a, 1)) catch unreachable; 146 | std.time.sleep(std.time.ns_per_ms * 10); 147 | ch.push(std.math.pow(u32, a, 2)) catch unreachable; 148 | std.time.sleep(std.time.ns_per_ms * 10); 149 | ch.push(std.math.pow(u32, a, 3)) catch unreachable; 150 | return a + b; 151 | } 152 | 153 | fn add(f1: *Future(?u128), f2: *Future(?u128)) ?u128 { 154 | const a = f1.wait().?; 155 | const b = f2.wait().?; 156 | return a + b; 157 | } 158 | }; 159 | 160 | { 161 | const TestTask = Task(S.div); 162 | var future = TestTask.launch(std.testing.allocator, .{ std.testing.allocator, 1, 0 }) catch unreachable; 163 | defer future.deinit(); 164 | try testing.expectError(error.DivisionByZero, future.wait()); 165 | try testing.expectError(error.DivisionByZero, future.timedWait(10).?); 166 | } 167 | 168 | { 169 | const TestTask = Task(S.div); 170 | var future = TestTask.launch(std.testing.allocator, .{ std.testing.allocator, 1, 1 }) catch unreachable; 171 | defer future.deinit(); 172 | const ret = future.wait(); 173 | try testing.expectEqual(@as(u32, 1), if (ret) |r| r.v else |_| unreachable); 174 | } 175 | 176 | { 177 | const TestTask = Task(S.return_nothing); 178 | var future = TestTask.launch(std.testing.allocator, .{}) catch unreachable; 179 | future.wait(); 180 | defer future.deinit(); 181 | } 182 | 183 | { 184 | var ch = try channel.Channel(u32).init(std.testing.allocator); 185 | defer ch.deinit(); 186 | 187 | const TestTask = Task(S.long_work); 188 | var future = TestTask.launch(std.testing.allocator, .{ ch, 2, 1 }) catch unreachable; 189 | defer future.deinit(); 190 | 191 | try testing.expectEqual(@as(?u32, null), future.timedWait(1)); 192 | try testing.expectEqual(@as(?u32, null), future.timedWait(1)); 193 | try testing.expectEqual(@as(?u32, null), future.timedWait(1)); 194 | try testing.expectEqual(@as(?u32, null), future.timedWait(1)); 195 | try testing.expectEqual(@as(?u32, null), future.timedWait(1)); 196 | try testing.expectEqual(@as(?u32, null), future.timedWait(1)); 197 | try testing.expectEqual(@as(?u32, null), future.timedWait(1)); 198 | try testing.expectEqual(@as(?u32, null), future.timedWait(1)); 199 | try testing.expectEqual(@as(?u32, null), ch.pop()); 200 | try testing.expectEqual(@as(u32, 3), future.wait()); 201 | 202 | var result = ch.popn(3).?; 203 | try testing.expectEqual(result.elements.items.len, 3); 204 | try testing.expectEqual(@as(u32, 2), result.elements.items[0]); 205 | try testing.expectEqual(@as(u32, 4), result.elements.items[1]); 206 | try testing.expectEqual(@as(u32, 8), result.elements.items[2]); 207 | result.deinit(); 208 | } 209 | 210 | { 211 | const TestTask = Task(S.add); 212 | var fs: [100]*TestTask.FutureType = undefined; 213 | fs[0] = try TestTask.FutureType.init(std.testing.allocator); 214 | fs[1] = try TestTask.FutureType.init(std.testing.allocator); 215 | fs[0].grant(0); 216 | fs[1].grant(1); 217 | 218 | // compute 100th fibonacci number 219 | var i: u32 = 2; 220 | while (i < 100) : (i += 1) { 221 | fs[i] = try TestTask.launch(std.testing.allocator, .{ fs[i - 2], fs[i - 1] }); 222 | } 223 | try testing.expectEqual(@as(u128, 218922995834555169026), fs[99].wait().?); 224 | for (fs) |f| f.deinit(); 225 | } 226 | } 227 | 228 | test "isSingleItemPtr" { 229 | const array = [_]u8{0} ** 10; 230 | comptime try testing.expect(isSingleItemPtr(@TypeOf(&array[0]))); 231 | comptime try testing.expect(!isSingleItemPtr(@TypeOf(array))); 232 | const runtime_zero: usize = 0; 233 | try testing.expect(!isSingleItemPtr(@TypeOf(array[runtime_zero..1]))); 234 | } 235 | --------------------------------------------------------------------------------