Canceling awaited group causes panic?

I have this code that causes panic on group.cancel. I can’t see anything wrong with the code though. Am I doing something wrong?

const std = @import("std");
const Io = std.Io;

pub fn main(init: std.process.Init) !void {
    var threaded: Io.Threaded = .init(init.gpa, .{});
    defer threaded.deinit();
    testIt(threaded.io());
}
fn testIt(io: Io) void {
    var idk: Idk = .{ .io = io, .group = .init };
    idk.group.concurrent(io, Idk.run, .{&idk}) catch @panic("no concurrency");
    io.sleep(.fromMilliseconds(1), .real) catch @panic("not reachable");
    idk.group.cancel(io);
}

const Idk = struct {
    io: Io,
    group: Io.Group,

    fn run(self: *Idk) void {
        self.group.concurrent(self.io, doSomeWork, .{self}) catch @panic("no concurrency");
        self.group.await(self.io) catch |err| switch (err) {
            error.Canceled => {
                self.io.recancel();
                return;
            },
        };
    }
    fn doSomeWork(self: *Idk) void {
        while (true) {
            self.io.sleep(.fromMilliseconds(10), .real) catch |err| switch (err) {
                error.Canceled => {
                    self.io.recancel();
                    return;
                },
            };
        }
    }
};

Stack trace:

thread 1498 panic: reached unreachable code
/opt/zig-x86_64-linux-0.16.0/lib/std/debug.zig:420:14: 0x102b6b9 in assert (std.zig)
    if (!ok) unreachable; // assertion failure
             ^
/opt/zig-x86_64-linux-0.16.0/lib/std/Io/Threaded.zig:2365:11: 0x119e0f4 in groupCancel (std.zig)
    assert(!pre_cancel_status.have_awaiter);
          ^
/opt/zig-x86_64-linux-0.16.0/lib/std/Io.zig:1300:30: 0x11d7683 in cancel (std.zig)
        io.vtable.groupCancel(io.userdata, g, token);
                             ^
/home/sysadmin/git/zig-playground/src/main.zig:12:21: 0x11d753a in testIt (main.zig)
    idk.group.cancel(io);
                    ^
/home/sysadmin/git/zig-playground/src/main.zig:6:11: 0x11d14ce in main (main.zig)
    testIt(threaded.io());
          ^
/opt/zig-x86_64-linux-0.16.0/lib/std/start.zig:737:30: 0x11d1cd7 in callMain (std.zig)
    return wrapMain(root.main(.{
                             ^
???:?:?: 0x71ab0e3e71c9 in ??? (/lib/x86_64-linux-gnu/libc.so.6)
???:?:?: 0x71ab0e3e728a in ??? (/lib/x86_64-linux-gnu/libc.so.6)
???:?:?: 0x1209a54 in ??? (???)
Aborted (core dumped)

you cant have multiple await/cancel’s on a task/group at the same time.

You also cant await/cancel from within the task/group itself, that will cause a deadlock.

Huh, so I can cancel awaited future but I can’t cancel awaited group? (I am not sure if you meant Future by the “task”, but canceling awaited future works fine) That seems awkward. If that is really expected behavior I feel like that should be documented in Group.await and Group.cancel

Also yes, that await is supposed to “deadlock” until the cancel.

EDIT: I was wrong. canceling awaited Future also causes panic. I guess I will have to abuse conditions or something to achieve what I want…

std.Io.Event is probably what your looking for.

In fact it’s allowed to do so, both await and cancel are idempotent, this allows for defer g.cancel(io) pattern to be used. The thing is that they’re not threadsafe, which is why it didn’t work in your case.

Well that is strange - I have also been getting the panic with zio (evented Io) when both await and cancel were running on the same thread. So it would seem the thread safety is not the issue (unless there was different reason for the panic with zio - but the stack trace looked about the same)

Yea this shouldn’t happen, unless you were copying the Io.Group itself? You can look at the implementation of Group.cancel and Group.await that is above the vtable, it checks if token is there, and only then calls to implementation.

I’m surprised about the panic in zio, but the general problem is, calling cancel or await inside the group is a deadlock. The operation has to wait until the group dies, but it can’t it you are blocking one task on await/cancel.

Okay, now that I am looking at it the stack trace doesn’t look same at all (I guess I just assumed that somehow because both panics were in the the std)…

The deadlock should not happen, since I am awaiting the group inside the group, which in my opinion should just make it wait until group.cancel is called (outside of the group) and then return error.Canceled - EDIT: Or so I believed, but I confirmed that awaiting group within the group indeed deadlocks at this branch - the other branch does not actually await within group but still crashes

Anyway, I’ve pushed the example - you can check it at https://codeberg.org/psznm/zig-playground/src/branch/group-panic

The zio panic is

thread 159777 panic: reached unreachable code
/opt/zig-x86_64-linux-0.16.0/lib/std/debug.zig:420:14: 0x106b6f9 in assert (std.zig)
    if (!ok) unreachable; // assertion failure
             ^
/opt/zig-x86_64-linux-0.16.0/lib/std/Io.zig:1285:15: 0x12224ca in await (std.zig)
        assert(g.token.raw == null);
              ^
/home/sysadmin/git/zig-playground/src/main.zig:28:25: 0x1222179 in run (main.zig)
/opt/zig-x86_64-linux-0.16.0/lib/std/Io.zig:2376:13: 0x1222011 in start (std.zig)
            result_casted.* = @call(.auto, function, args_casted.*);
            ^
/home/sysadmin/git/zig-playground/zig-pkg/zio-0.17.0-xHbVVPTjJQBdqofztUjZuuQXiXE4dAbXc2chqH-OjFDp/src/task.zig:73:22: 0x135f745 in call__anon_57684 (zio.zig)
/home/sysadmin/git/zig-playground/zig-pkg/zio-0.17.0-xHbVVPTjJQBdqofztUjZuuQXiXE4dAbXc2chqH-OjFDp/src/task.zig:496:26: 0x135f52c in startFn (zio.zig)
/home/sysadmin/git/zig-playground/zig-pkg/zio-0.17.0-xHbVVPTjJQBdqofztUjZuuQXiXE4dAbXc2chqH-OjFDp/src/coro/coroutines.zig:1579:31: 0x135fb6a in entrypointFn (zio.zig)
Aborted (core dumped)

Ok, now I see, this is essentially the same problem, just different semantics of the check. Zio uses the token as pointer to the task list, and it pops all tasks at the beginning of cancel. So when you hit the await, the token is already null and so it panics inside std.Io.Group.

Really, you can only safely await or cancel the group from the thread where you initiated it. Anything else will lead to problems. These checks are just avoiding the deadlock.

you can only safely await or cancel the group from the thread where you initiated it

It is the same thread though? .{ .executors = .exact(1), .thread_pool = .{ .max_threads = 0 } }

Or you mean this also means same green thread?

Sorry, I meant thread in the abstract concept, as in “thread of execution”. You always need to have one task owning the group, and only that one is doing either cancel or await. If you need synchronization from within the group, use event/condition/queue.