#Channels in Zig

1 messages · Page 1 of 1 (latest)

sage hill
#

I'm trying to implement a very barebones channel with the greater goal of rewriting some Go code in Zig.

With the following code my test just hangs. I thought receiving the messages in a separate thread would stop it from blocking but that is not the case. Anyone know what I'm doing wrong or if there is something better in the standard library I don't know about?

#
pub fn Channel(comptime T: type) type {
    return struct {
        const Self = @This();

        allocator: mem.Allocator,
        messages: ArrayList(T),
        mutex: Thread.Mutex,
        condition: Thread.Condition,
        is_closed: bool,

        pub fn open(allocator: mem.Allocator) Self {
            return Self{
                .allocator = allocator,
                .messages = ArrayList(T).init(allocator),
                .mutex = Thread.Mutex{},
                .condition = Thread.Condition{},
                .is_closed = false,
            };
        }

        pub fn close(self: *Self) void {
            self.mutex.lock();
            defer self.mutex.unlock();
            self.is_closed = true;
            self.messages.deinit();
        }

        pub fn send(self: *Self, message: T) !void {
            self.mutex.lock();
            defer self.mutex.unlock();

            if (self.is_closed)
                return error.ChannelClosed;

            try self.messages.append(message);
        }

        pub fn receive(self: *Self) !?T {
            self.mutex.lock();
            defer self.mutex.unlock();

            if (self.is_closed)
                return error.ChannelClosed;

            if (self.messages.items.len > 0)
                return self.messages.pop();

            while (!self.is_closed and self.messages.items.len == 0)
                self.condition.wait(&self.mutex);

            if (self.is_closed)
                return error.ChannelClosed;

            if (self.messages.items.len > 0)
                return self.messages.pop();

            return null;
        }
    };
}
#

Here is the test that hangs

const testing = std.testing;

fn receiveMessage(channel: *Channel(u8)) void {
    while (channel.receive()) |message|
        std.debug.print("Received message: {}\n", .{message});
    channel.close();
}

test "Channel" {
    const allocator = testing.allocator;
    var channel = Channel(u8).open(allocator);

    const thread = try Thread.spawn(.{}, receiveMessage, .{&channel});
    defer thread.join();

    try channel.send(1);
}

amber marlin
#

Looks like you don't signal the Thread.Condition?

sage hill
#

Ok yeah I added self.condition.signal(); after appending the message now but it still hangs

rigid hazel
#

btw with ring buffer could implement channel with atomics so no need for exclusive locking. Though you'd need to have a fixed size for the ring buffer

#

I think the problem is that receive end locks the mutex and then indefinitely starts waiting on the condition, which can only be relesed by the other thread that is now locked out of mutex

#

deadlock

amber marlin
#

That should be fine I think

#

Thread.Condition unlocks the mutex while it wairs

rigid hazel
#

ah i see, just noticed the reference to mutex passed to condition

amber marlin
#

Maybe the issue is that the channel isn't closed?

#

You receive in a loop but only send once

#

It should probably be closed on the sender's side, so that the receiver will get is_closed = true

rigid hazel
#

probably in this case defer channel close right after channel creation would be most fitting

amber marlin
#

Does it hang before even printing anything? Can't run it myself to test rn

sage hill
#

It was because the channel wasn't closed. I moved call to channel.close() inside the while loop and now it prints that it receives the message.

I still don't understand why it didn't print anything before though, since it still should have received the message before hanging

rigid hazel
#

Might be that test collects the messages and prints them once it's done executing, don't remember