#producer consumer patterns

1 messages · Page 1 of 1 (latest)

prime wagon
#

I'm on zig 0.12.1.
I have 2 versions of code. One of these versions gives me what you would expect, and the other version leads to undefined behavior/the consumer just not running. The only difference between them is in the init functions.

Does anyone know why this is?

#

Working version

const std = @import("std");

const ProdCons = struct {
    const Self = @This();
    const QT = std.PriorityQueue(u32, void, order);
    x: u32 = 0,
    s: std.Thread.Semaphore = .{ .permits = 0 },
    q: QT = QT.init(std.heap.page_allocator, {}),
    c: std.Thread = undefined,

    pub fn init() !Self {
        return Self{};
    }

    fn order(_: void, a: u32, b: u32) std.math.Order {
        return std.math.order(a, b);
    }

    pub fn producer(self: *Self, a: u32) void {
        std.debug.print("producer posts\n", .{});
        self.q.add(a) catch unreachable;
        self.s.post();
    }

    fn consumer(self: *Self) void {
        while (true) {
            self.s.wait();
            const a = self.q.remove();
            std.debug.print("consumer consooms\n", .{});
            self.x += a;
            std.debug.print("x: {}\n", .{self.x});
        }
    }
};

pub fn main() !void {
    var pc = try ProdCons.init();
    const c = try std.Thread.spawn(.{}, ProdCons.consumer, .{&pc});
    c.detach();
    std.debug.print("Hello, World!\n", .{});
    for (0..10) |i| {
        std.time.sleep(@as(usize, @intFromFloat(0.05 * std.time.ns_per_s)) * i);
        pc.producer(@intCast(i));
    }
    std.time.sleep(0.25 * std.time.ns_per_s);
}
#

Buggy version

const std = @import("std");

const ProdCons = struct {
    const Self = @This();
    const QT = std.PriorityQueue(u32, void, order);
    x: u32 = 0,
    s: std.Thread.Semaphore = .{ .permits = 0 },
    q: QT = QT.init(std.heap.page_allocator, {}),
    c: std.Thread = undefined,

    pub fn init() !Self {
        var pc = Self{};
        pc.c = try std.Thread.spawn(.{}, ProdCons.consumer, .{&pc});
        return pc;
    }

    fn order(_: void, a: u32, b: u32) std.math.Order {
        return std.math.order(a, b);
    }

    pub fn producer(self: *Self, a: u32) void {
        std.debug.print("producer posts\n", .{});
        self.q.add(a) catch unreachable;
        self.s.post();
    }

    fn consumer(self: *Self) void {
        while (true) {
            self.s.wait();
            const a = self.q.remove();
            std.debug.print("consumer consooms\n", .{});
            self.x += a;
            std.debug.print("x: {}\n", .{self.x});
        }
    }
};

pub fn main() !void {
    var pc = try ProdCons.init();
    std.debug.print("Hello, World!\n", .{});
    for (0..10) |i| {
        std.time.sleep(@as(usize, @intFromFloat(0.05 * std.time.ns_per_s)) * i);
        pc.producer(@intCast(i));
    }
    std.time.sleep(0.25 * std.time.ns_per_s);
}
lofty prairie
#

itll only work sometimes as PriorityQueue isnt thread safe. You need to access it around something like a Mutex.

#

in the second version, a pointer to the pc local variable is passed in, but then that local variable is destroyed when the function returns. It needs to stay alive & pinned while the thread is running

prime wagon
#

Is the semaphor I have not enough to make it thread safe?

lofty prairie
#

not for multiple items. Heres an invalid interleaving:

producer:
  queue.push()
  sema.post()
consumer:
  sema.wait() woken up
  preempted during queue.pop(), queue temporarily in invalid state
producer:
  queue.push() on invalid queue
prime wagon
#

Ah I see. Yeah it makes sense now. Though for the second point, how do you keep it alive and pinned? Is it just that I need to put it on the heap?

lofty prairie
#

putting it on the heap is one way. Keeping the variable alive while the thread is spawned & until its joined / stops using it (like in version1) is another