zig/lib/std/event/channel.zig

335 lines
13 KiB
Zig
Raw Normal View History

2019-03-02 21:46:04 +00:00
const std = @import("../std.zig");
const builtin = @import("builtin");
2018-07-10 03:22:44 +01:00
const assert = std.debug.assert;
const testing = std.testing;
2018-07-10 03:22:44 +01:00
const Loop = std.event.Loop;
std lib networking improvements, especially non-blocking I/O * delete the std/event/net directory * `std.event.Loop.waitUntilFdReadable` and related functions no longer have possibility of failure. On Linux, they fall back to poll() and then fall back to sleep(). * add some missing `noasync` decorations in `std.event.Loop` * redo the `std.net.Server` API. it's quite nice now, but shutdown does not work cleanly. There is a race condition with close() that I am actively working on. * move `std.io.OutStream` to its own file to match `std.io.InStream`. I started working on making `write` integrated with evented I/O, but it got tricky so I backed off and filed #3557. However I did integrate `std.os.writev` and `std.os.pwritev` with evented I/O. * add `std.Target.stack_align` * move networking tests to `lib/std/net/test.zig` * add `std.net.tcpConnectToHost` and `std.net.tcpConnectToAddress`. * rename `error.UnknownName` to `error.UnknownHostName` within the context of DNS resolution. * add `std.os.readv`, which is integrated with evented I/O. * `std.os.preadv`, is now integrated with evented I/O. * `std.os.accept4` now asserts that ENOTSOCK and EOPNOTSUPP never occur (misuse of API), instead of returning errors. * `std.os.connect` is now integrated with evented I/O. `std.os.connect_async` is gone. Just use `std.os.connect`. * fix false positive dependency loop regarding async function frames * add more compile notes to help when dependency loops occur in determining whether a function is async. * ir: change an assert to ir_assert to make it easier to find workarounds for when such an assert is triggered. In this case it was trying to parse an IPv4 address at comptime.
2019-10-30 02:59:30 +00:00
/// Many producer, many consumer, thread-safe, runtime configurable buffer size.
/// When buffer is empty, consumers suspend and are resumed by producers.
/// When buffer is full, producers suspend and are resumed by consumers.
2018-07-10 03:22:44 +01:00
pub fn Channel(comptime T: type) type {
return struct {
getters: std.atomic.Queue(GetNode),
2018-08-02 22:04:17 +01:00
or_null_queue: std.atomic.Queue(*std.atomic.Queue(GetNode).Node),
putters: std.atomic.Queue(PutNode),
2018-07-10 03:22:44 +01:00
get_count: usize,
put_count: usize,
2020-03-10 20:46:19 +00:00
dispatch_lock: bool,
need_dispatch: bool,
2018-07-10 03:22:44 +01:00
// simple fixed size ring buffer
buffer_nodes: []T,
buffer_index: usize,
buffer_len: usize,
const SelfChannel = @This();
const GetNode = struct {
2018-07-10 03:22:44 +01:00
tick_node: *Loop.NextTickNode,
2018-08-02 22:04:17 +01:00
data: Data,
const Data = union(enum) {
2018-08-02 22:04:17 +01:00
Normal: Normal,
OrNull: OrNull,
};
const Normal = struct {
2018-08-02 22:04:17 +01:00
ptr: *T,
};
const OrNull = struct {
2018-08-02 22:04:17 +01:00
ptr: *?T,
or_null: *std.atomic.Queue(*std.atomic.Queue(GetNode).Node).Node,
};
2018-07-10 03:22:44 +01:00
};
const PutNode = struct {
2018-07-10 03:22:44 +01:00
data: T,
tick_node: *Loop.NextTickNode,
};
const global_event_loop = Loop.instance orelse
@compileError("std.event.Channel currently only works with event-based I/O");
2018-07-10 03:22:44 +01:00
/// Call `deinit` to free resources when done.
/// `buffer` must live until `deinit` is called.
/// For a zero length buffer, use `[0]T{}`.
/// TODO https://github.com/ziglang/zig/issues/2765
pub fn init(self: *SelfChannel, buffer: []T) void {
// The ring buffer implementation only works with power of 2 buffer sizes
// because of relying on subtracting across zero. For example (0 -% 1) % 10 == 5
assert(buffer.len == 0 or @popCount(usize, buffer.len) == 1);
self.* = SelfChannel{
2018-07-10 03:22:44 +01:00
.buffer_len = 0,
.buffer_nodes = buffer,
2018-07-10 03:22:44 +01:00
.buffer_index = 0,
2020-03-10 20:46:19 +00:00
.dispatch_lock = false,
.need_dispatch = false,
.getters = std.atomic.Queue(GetNode).init(),
.putters = std.atomic.Queue(PutNode).init(),
2018-08-02 22:04:17 +01:00
.or_null_queue = std.atomic.Queue(*std.atomic.Queue(GetNode).Node).init(),
2018-07-10 03:22:44 +01:00
.get_count = 0,
.put_count = 0,
};
2018-07-10 03:22:44 +01:00
}
/// Must be called when all calls to put and get have suspended and no more calls occur.
/// This can be omitted if caller can guarantee that the suspended putters and getters
/// do not need to be run to completion. Note that this may leave awaiters hanging.
pub fn deinit(self: *SelfChannel) void {
2018-07-10 03:22:44 +01:00
while (self.getters.get()) |get_node| {
2019-08-12 00:53:10 +01:00
resume get_node.data.tick_node.data;
2018-07-10 03:22:44 +01:00
}
while (self.putters.get()) |put_node| {
2019-08-12 00:53:10 +01:00
resume put_node.data.tick_node.data;
2018-07-10 03:22:44 +01:00
}
self.* = undefined;
2018-07-10 03:22:44 +01:00
}
2019-08-12 00:53:10 +01:00
/// puts a data item in the channel. The function returns when the value has been added to the
2018-07-10 03:22:44 +01:00
/// buffer, or in the case of a zero size buffer, when the item has been retrieved by a getter.
2019-08-12 00:53:10 +01:00
/// Or when the channel is destroyed.
pub fn put(self: *SelfChannel, data: T) void {
var my_tick_node = Loop.NextTickNode{ .data = @frame() };
var queue_node = std.atomic.Queue(PutNode).Node{
.data = PutNode{
.tick_node = &my_tick_node,
.data = data,
},
};
2018-08-02 22:04:17 +01:00
suspend {
2018-07-10 03:22:44 +01:00
self.putters.put(&queue_node);
_ = @atomicRmw(usize, &self.put_count, .Add, 1, .SeqCst);
2018-07-10 03:22:44 +01:00
self.dispatch();
2018-07-10 03:22:44 +01:00
}
}
2019-08-12 00:53:10 +01:00
/// await this function to get an item from the channel. If the buffer is empty, the frame will
2018-07-10 03:22:44 +01:00
/// complete when the next item is put in the channel.
2020-05-04 16:49:27 +01:00
pub fn get(self: *SelfChannel) callconv(.Async) T {
// TODO https://github.com/ziglang/zig/issues/2765
2018-07-10 03:22:44 +01:00
var result: T = undefined;
var my_tick_node = Loop.NextTickNode{ .data = @frame() };
var queue_node = std.atomic.Queue(GetNode).Node{
.data = GetNode{
.tick_node = &my_tick_node,
.data = GetNode.Data{
.Normal = GetNode.Normal{ .ptr = &result },
},
2018-08-02 22:04:17 +01:00
},
};
2018-08-02 22:04:17 +01:00
suspend {
2018-07-10 03:22:44 +01:00
self.getters.put(&queue_node);
_ = @atomicRmw(usize, &self.get_count, .Add, 1, .SeqCst);
2018-07-10 03:22:44 +01:00
self.dispatch();
2018-07-10 03:22:44 +01:00
}
return result;
}
2018-08-02 22:04:17 +01:00
//pub async fn select(comptime EnumUnion: type, channels: ...) EnumUnion {
// assert(@memberCount(EnumUnion) == channels.len); // enum union and channels mismatch
// assert(channels.len != 0); // enum unions cannot have 0 fields
// if (channels.len == 1) {
// const result = await (async channels[0].get() catch unreachable);
// return @unionInit(EnumUnion, @memberName(EnumUnion, 0), result);
// }
//}
/// Get an item from the channel. If the buffer is empty and there are no
/// puts waiting, this returns `null`.
pub fn getOrNull(self: *SelfChannel) ?T {
2018-08-02 22:04:17 +01:00
// TODO integrate this function with named return values
// so we can get rid of this extra result copy
var result: ?T = null;
var my_tick_node = Loop.NextTickNode{ .data = @frame() };
var or_null_node = std.atomic.Queue(*std.atomic.Queue(GetNode).Node).Node{ .data = undefined };
var queue_node = std.atomic.Queue(GetNode).Node{
.data = GetNode{
.tick_node = &my_tick_node,
.data = GetNode.Data{
.OrNull = GetNode.OrNull{
.ptr = &result,
.or_null = &or_null_node,
},
2018-08-02 22:04:17 +01:00
},
},
};
2018-08-02 22:04:17 +01:00
or_null_node.data = &queue_node;
suspend {
2018-08-02 22:04:17 +01:00
self.getters.put(&queue_node);
_ = @atomicRmw(usize, &self.get_count, .Add, 1, .SeqCst);
2018-08-02 22:04:17 +01:00
self.or_null_queue.put(&or_null_node);
self.dispatch();
}
return result;
}
fn dispatch(self: *SelfChannel) void {
2018-07-10 03:22:44 +01:00
// set the "need dispatch" flag
2020-03-10 20:46:19 +00:00
@atomicStore(bool, &self.need_dispatch, true, .SeqCst);
2018-07-10 03:22:44 +01:00
lock: while (true) {
// set the lock flag
if (@atomicRmw(bool, &self.dispatch_lock, .Xchg, true, .SeqCst)) return;
2018-07-10 03:22:44 +01:00
// clear the need_dispatch flag since we're about to do it
2020-03-10 20:46:19 +00:00
@atomicStore(bool, &self.need_dispatch, false, .SeqCst);
2018-07-10 03:22:44 +01:00
while (true) {
one_dispatch: {
// later we correct these extra subtractions
var get_count = @atomicRmw(usize, &self.get_count, .Sub, 1, .SeqCst);
var put_count = @atomicRmw(usize, &self.put_count, .Sub, 1, .SeqCst);
2018-07-10 03:22:44 +01:00
// transfer self.buffer to self.getters
while (self.buffer_len != 0) {
if (get_count == 0) break :one_dispatch;
const get_node = &self.getters.get().?.data;
2018-08-02 22:04:17 +01:00
switch (get_node.data) {
GetNode.Data.Normal => |info| {
info.ptr.* = self.buffer_nodes[(self.buffer_index -% self.buffer_len) % self.buffer_nodes.len];
2018-08-02 22:04:17 +01:00
},
GetNode.Data.OrNull => |info| {
_ = self.or_null_queue.remove(info.or_null);
info.ptr.* = self.buffer_nodes[(self.buffer_index -% self.buffer_len) % self.buffer_nodes.len];
2018-08-02 22:04:17 +01:00
},
}
global_event_loop.onNextTick(get_node.tick_node);
2018-07-10 03:22:44 +01:00
self.buffer_len -= 1;
get_count = @atomicRmw(usize, &self.get_count, .Sub, 1, .SeqCst);
2018-07-10 03:22:44 +01:00
}
// direct transfer self.putters to self.getters
while (get_count != 0 and put_count != 0) {
const get_node = &self.getters.get().?.data;
const put_node = &self.putters.get().?.data;
2018-08-02 22:04:17 +01:00
switch (get_node.data) {
GetNode.Data.Normal => |info| {
info.ptr.* = put_node.data;
},
GetNode.Data.OrNull => |info| {
_ = self.or_null_queue.remove(info.or_null);
info.ptr.* = put_node.data;
},
}
global_event_loop.onNextTick(get_node.tick_node);
global_event_loop.onNextTick(put_node.tick_node);
2018-07-10 03:22:44 +01:00
get_count = @atomicRmw(usize, &self.get_count, .Sub, 1, .SeqCst);
put_count = @atomicRmw(usize, &self.put_count, .Sub, 1, .SeqCst);
2018-07-10 03:22:44 +01:00
}
// transfer self.putters to self.buffer
while (self.buffer_len != self.buffer_nodes.len and put_count != 0) {
const put_node = &self.putters.get().?.data;
self.buffer_nodes[self.buffer_index % self.buffer_nodes.len] = put_node.data;
global_event_loop.onNextTick(put_node.tick_node);
2018-07-10 03:22:44 +01:00
self.buffer_index +%= 1;
self.buffer_len += 1;
put_count = @atomicRmw(usize, &self.put_count, .Sub, 1, .SeqCst);
2018-07-10 03:22:44 +01:00
}
}
// undo the extra subtractions
_ = @atomicRmw(usize, &self.get_count, .Add, 1, .SeqCst);
_ = @atomicRmw(usize, &self.put_count, .Add, 1, .SeqCst);
2018-07-10 03:22:44 +01:00
2018-08-02 22:04:17 +01:00
// All the "get or null" functions should resume now.
var remove_count: usize = 0;
while (self.or_null_queue.get()) |or_null_node| {
remove_count += @boolToInt(self.getters.remove(or_null_node.data));
global_event_loop.onNextTick(or_null_node.data.data.tick_node);
2018-08-02 22:04:17 +01:00
}
if (remove_count != 0) {
_ = @atomicRmw(usize, &self.get_count, .Sub, remove_count, .SeqCst);
2018-08-02 22:04:17 +01:00
}
2018-07-10 03:22:44 +01:00
// clear need-dispatch flag
if (@atomicRmw(bool, &self.need_dispatch, .Xchg, false, .SeqCst)) continue;
2018-07-10 03:22:44 +01:00
assert(@atomicRmw(bool, &self.dispatch_lock, .Xchg, false, .SeqCst));
2018-07-10 03:22:44 +01:00
// we have to check again now that we unlocked
2020-03-10 20:46:19 +00:00
if (@atomicLoad(bool, &self.need_dispatch, .SeqCst)) continue :lock;
2018-07-10 03:22:44 +01:00
return;
}
}
}
};
}
test "std.event.Channel" {
2020-02-08 21:24:26 +00:00
if (!std.io.is_async) return error.SkipZigTest;
// https://github.com/ziglang/zig/issues/1908
if (builtin.single_threaded) return error.SkipZigTest;
// https://github.com/ziglang/zig/issues/3251
if (builtin.os.tag == .freebsd) return error.SkipZigTest;
var channel: Channel(i32) = undefined;
2020-02-08 21:24:26 +00:00
channel.init(&[0]i32{});
defer channel.deinit();
2018-07-10 03:22:44 +01:00
var handle = async testChannelGetter(&channel);
var putter = async testChannelPutter(&channel);
2018-07-10 03:22:44 +01:00
await handle;
await putter;
2018-07-10 03:22:44 +01:00
}
test "std.event.Channel wraparound" {
// TODO provide a way to run tests in evented I/O mode
if (!std.io.is_async) return error.SkipZigTest;
const channel_size = 2;
var buf: [channel_size]i32 = undefined;
var channel: Channel(i32) = undefined;
channel.init(&buf);
defer channel.deinit();
// add items to channel and pull them out until
// the buffer wraps around, make sure it doesn't crash.
channel.put(5);
2021-05-04 18:47:26 +01:00
try testing.expectEqual(@as(i32, 5), channel.get());
channel.put(6);
2021-05-04 18:47:26 +01:00
try testing.expectEqual(@as(i32, 6), channel.get());
channel.put(7);
2021-05-04 18:47:26 +01:00
try testing.expectEqual(@as(i32, 7), channel.get());
}
2020-05-04 16:49:27 +01:00
fn testChannelGetter(channel: *Channel(i32)) callconv(.Async) void {
const value1 = channel.get();
2021-05-04 18:47:26 +01:00
try testing.expect(value1 == 1234);
2018-07-10 03:22:44 +01:00
const value2 = channel.get();
2021-05-04 18:47:26 +01:00
try testing.expect(value2 == 4567);
2018-08-02 22:04:17 +01:00
const value3 = channel.getOrNull();
2021-05-04 18:47:26 +01:00
try testing.expect(value3 == null);
2018-08-02 22:04:17 +01:00
var last_put = async testPut(channel, 4444);
2019-08-08 21:41:38 +01:00
const value4 = channel.getOrNull();
2021-05-04 18:47:26 +01:00
try testing.expect(value4.? == 4444);
2018-08-02 22:04:17 +01:00
await last_put;
2018-07-10 03:22:44 +01:00
}
2020-05-04 16:49:27 +01:00
fn testChannelPutter(channel: *Channel(i32)) callconv(.Async) void {
2019-08-08 21:41:38 +01:00
channel.put(1234);
channel.put(4567);
2018-07-10 03:22:44 +01:00
}
2020-05-04 16:49:27 +01:00
fn testPut(channel: *Channel(i32), value: i32) callconv(.Async) void {
2019-08-08 21:41:38 +01:00
channel.put(value);
2018-08-02 22:04:17 +01:00
}