async: add intrusive queue
all code contributed by @steeve * add intrusive queue * change the constructor of Channel with default AsyncThread executor --------- Co-authored-by: Steeve Morin <steeve@zml.ai>
This commit is contained in:
parent
98b512c495
commit
27c8309424
@ -3,6 +3,7 @@ load("@rules_zig//zig:defs.bzl", "zig_library")
|
|||||||
zig_library(
|
zig_library(
|
||||||
name = "async",
|
name = "async",
|
||||||
srcs = [
|
srcs = [
|
||||||
|
"queue.zig",
|
||||||
"queue_mpsc.zig",
|
"queue_mpsc.zig",
|
||||||
],
|
],
|
||||||
import_name = "async",
|
import_name = "async",
|
||||||
|
|||||||
101
async/queue.zig
Normal file
101
async/queue.zig
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
|
||||||
|
/// An intrusive queue implementation. The type T must have a field
|
||||||
|
/// "next" of type `?*T`.
|
||||||
|
///
|
||||||
|
/// For those unaware, an intrusive variant of a data structure is one in which
|
||||||
|
/// the data type in the list has the pointer to the next element, rather
|
||||||
|
/// than a higher level "node" or "container" type. The primary benefit
|
||||||
|
/// of this (and the reason we implement this) is that it defers all memory
|
||||||
|
/// management to the caller: the data structure implementation doesn't need
|
||||||
|
/// to allocate "nodes" to contain each element. Instead, the caller provides
|
||||||
|
/// the element and how its allocated is up to them.
|
||||||
|
pub fn Intrusive(comptime T: type) type {
|
||||||
|
return struct {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
/// Head is the front of the queue and tail is the back of the queue.
|
||||||
|
head: ?*T = null,
|
||||||
|
tail: ?*T = null,
|
||||||
|
|
||||||
|
/// Enqueue a new element to the back of the queue.
|
||||||
|
pub fn push(self: *Self, v: *T) void {
|
||||||
|
assert(v.next == null);
|
||||||
|
|
||||||
|
if (self.tail) |tail| {
|
||||||
|
// If we have elements in the queue, then we add a new tail.
|
||||||
|
tail.next = v;
|
||||||
|
self.tail = v;
|
||||||
|
} else {
|
||||||
|
// No elements in the queue we setup the initial state.
|
||||||
|
self.head = v;
|
||||||
|
self.tail = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dequeue the next element from the queue.
|
||||||
|
pub fn pop(self: *Self) ?*T {
|
||||||
|
// The next element is in "head".
|
||||||
|
const next = self.head orelse return null;
|
||||||
|
|
||||||
|
// If the head and tail are equal this is the last element
|
||||||
|
// so we also set tail to null so we can now be empty.
|
||||||
|
if (self.head == self.tail) self.tail = null;
|
||||||
|
|
||||||
|
// Head is whatever is next (if we're the last element,
|
||||||
|
// this will be null);
|
||||||
|
self.head = next.next;
|
||||||
|
|
||||||
|
// We set the "next" field to null so that this element
|
||||||
|
// can be inserted again.
|
||||||
|
next.next = null;
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the queue is empty.
|
||||||
|
pub fn empty(self: *const Self) bool {
|
||||||
|
return self.head == null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test Intrusive {
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
// Types
|
||||||
|
const Elem = struct {
|
||||||
|
const Self = @This();
|
||||||
|
next: ?*Self = null,
|
||||||
|
};
|
||||||
|
const Queue = Intrusive(Elem);
|
||||||
|
var q: Queue = .{};
|
||||||
|
try testing.expect(q.empty());
|
||||||
|
|
||||||
|
// Elems
|
||||||
|
var elems: [10]Elem = .{.{}} ** 10;
|
||||||
|
|
||||||
|
// One
|
||||||
|
try testing.expect(q.pop() == null);
|
||||||
|
q.push(&elems[0]);
|
||||||
|
try testing.expect(!q.empty());
|
||||||
|
try testing.expect(q.pop().? == &elems[0]);
|
||||||
|
try testing.expect(q.pop() == null);
|
||||||
|
try testing.expect(q.empty());
|
||||||
|
|
||||||
|
// Two
|
||||||
|
try testing.expect(q.pop() == null);
|
||||||
|
q.push(&elems[0]);
|
||||||
|
q.push(&elems[1]);
|
||||||
|
try testing.expect(q.pop().? == &elems[0]);
|
||||||
|
try testing.expect(q.pop().? == &elems[1]);
|
||||||
|
try testing.expect(q.pop() == null);
|
||||||
|
|
||||||
|
// Interleaved
|
||||||
|
try testing.expect(q.pop() == null);
|
||||||
|
q.push(&elems[0]);
|
||||||
|
try testing.expect(q.pop().? == &elems[0]);
|
||||||
|
q.push(&elems[1]);
|
||||||
|
try testing.expect(q.pop().? == &elems[1]);
|
||||||
|
try testing.expect(q.pop() == null);
|
||||||
|
}
|
||||||
@ -5,6 +5,51 @@ const libcoro = @import("libcoro");
|
|||||||
const aio = libcoro.asyncio;
|
const aio = libcoro.asyncio;
|
||||||
const queue_mpsc = @import("queue_mpsc.zig");
|
const queue_mpsc = @import("queue_mpsc.zig");
|
||||||
|
|
||||||
|
pub const Queue = @import("queue.zig").Intrusive;
|
||||||
|
|
||||||
|
pub const Condition = struct {
|
||||||
|
const CoroResume = struct {
|
||||||
|
coro: libcoro.Frame,
|
||||||
|
|
||||||
|
fn init() CoroResume {
|
||||||
|
return .{ .coro = libcoro.xframe() };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn func(self: *CoroResume) libcoro.Executor.Func {
|
||||||
|
return .{ .func = CoroResume.cb, .userdata = self };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cb(ud: ?*anyopaque) void {
|
||||||
|
const self: *CoroResume = @ptrCast(@alignCast(ud));
|
||||||
|
libcoro.xresume(self.coro);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exec: *libcoro.Executor,
|
||||||
|
waiters: Queue(libcoro.Executor.Func) = .{},
|
||||||
|
|
||||||
|
pub fn init() Condition {
|
||||||
|
return .{ .exec = &AsyncThread.current.executor.exec };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn broadcast(self: *Condition) void {
|
||||||
|
while (self.waiters.pop()) |waiter| {
|
||||||
|
self.exec.runSoon(waiter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn signal(self: *Condition) void {
|
||||||
|
if (self.waiters.pop()) |waiter| self.exec.runSoon(waiter);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn wait(self: *Condition) void {
|
||||||
|
var res = CoroResume.init();
|
||||||
|
var cb = res.func();
|
||||||
|
self.waiters.push(&cb);
|
||||||
|
libcoro.xsuspend();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
pub fn Frame(comptime func: anytype) type {
|
pub fn Frame(comptime func: anytype) type {
|
||||||
const Signature = stdx.meta.FnSignature(func, null);
|
const Signature = stdx.meta.FnSignature(func, null);
|
||||||
return FrameExx(func, Signature.ArgsT, Signature.ReturnT);
|
return FrameExx(func, Signature.ArgsT, Signature.ReturnT);
|
||||||
@ -482,6 +527,31 @@ pub const Socket = struct {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub fn Channel(comptime T: type, capacity: usize) type {
|
||||||
|
return struct {
|
||||||
|
const Self = @This();
|
||||||
|
const Inner = libcoro.Channel(T, .{ .capacity = capacity });
|
||||||
|
|
||||||
|
inner: Inner,
|
||||||
|
|
||||||
|
pub fn init() Self {
|
||||||
|
return .{ .inner = Inner.init(&AsyncThread.current.executor.exec) };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn close(self: *Self) void {
|
||||||
|
self.inner.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send(self: *Self, val: T) void {
|
||||||
|
self.inner.send(val) catch unreachable;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn recv(self: *Self) ?T {
|
||||||
|
return self.inner.recv();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
pub const Mutex = struct {
|
pub const Mutex = struct {
|
||||||
const VoidChannel = libcoro.Channel(void, .{ .capacity = 1 });
|
const VoidChannel = libcoro.Channel(void, .{ .capacity = 1 });
|
||||||
|
|
||||||
|
|||||||
@ -47,37 +47,28 @@ pub fn ArgsTuple(comptime funcT: anytype, comptime ArgsT: ?type) type {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn FnSignature(comptime func: anytype, comptime ArgsT: ?type) type {
|
pub const Signature = struct {
|
||||||
const n_params = switch (@typeInfo(@TypeOf(func))) {
|
FuncT: type,
|
||||||
.Fn => |fn_info| fn_info.params.len,
|
ArgsT: type,
|
||||||
else => compileError("FnSignature expects a function as first argument got: {}", .{@TypeOf(func)}),
|
ReturnT: type,
|
||||||
};
|
ReturnPayloadT: type,
|
||||||
if (ArgsT != null) {
|
ReturnErrorSet: ?type,
|
||||||
const n_args = switch (@typeInfo(ArgsT.?)) {
|
};
|
||||||
.Struct => |struct_info| struct_info.fields.len,
|
|
||||||
else => compileError("function {} need to be called with a tuple of args", .{@TypeOf(func)}),
|
|
||||||
};
|
|
||||||
if (n_params != n_args) {
|
|
||||||
compileError("function {} expected {} args, got {}", .{ @TypeOf(func), n_params, n_args });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return FnSignatureX(func, ArgsTuple(@TypeOf(func), ArgsT));
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: I think this should return a struct instead of returing at type
|
pub fn FnSignature(comptime func: anytype, comptime argsT_: ?type) Signature {
|
||||||
// this gives a better error stacktrace because here the error is delayed to when the fields are read.
|
const argsT = ArgsTuple(@TypeOf(func), argsT_);
|
||||||
fn FnSignatureX(comptime func: anytype, comptime ArgsT_: type) type {
|
const return_type = @TypeOf(@call(.auto, func, @as(argsT, undefined)));
|
||||||
return struct {
|
return Signature{
|
||||||
pub const FuncT = @TypeOf(func);
|
.FuncT = @TypeOf(func),
|
||||||
pub const ArgsT = ArgsT_;
|
.ArgsT = argsT,
|
||||||
pub const ReturnT = @TypeOf(@call(.auto, func, @as(ArgsT_, undefined)));
|
.ReturnT = return_type,
|
||||||
pub const ReturnPayloadT = switch (@typeInfo(ReturnT)) {
|
.ReturnPayloadT = switch (@typeInfo(return_type)) {
|
||||||
.ErrorUnion => |u| u.payload,
|
.ErrorUnion => |u| u.payload,
|
||||||
else => ReturnT,
|
else => return_type,
|
||||||
};
|
},
|
||||||
pub const ReturnErrorSet: ?type = switch (@typeInfo(ReturnT)) {
|
.ReturnErrorSet = switch (@typeInfo(return_type)) {
|
||||||
.ErrorUnion => |u| u.error_set,
|
.ErrorUnion => |u| u.error_set,
|
||||||
else => null,
|
else => null,
|
||||||
};
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user