Zig’s Io.Threaded is Neat
std.Io.Threaded is one of the implementations of Zig’s new Io interface that enables concurrency. This is a boring “just use threads” impl. I personally find it neat though — it does this weird thing that I wanted to do for ages, that to my knowledge no one else is doing properly, and implements it better than I thought to be possible.
Io.Threaded uses blocking syscalls and fully supports cancelation.
Concurrency vs Parallelism Quoting @tedinski, Concurrency is about handling (asynchronous, nondeterministic) events.
Parallelism is about using hardware resources to do more at the same time. I think this definition is correct, but doesn’t provide useful intuition directly. Concurrency is the same thing as state transducers? Yes, obviously, but not really illuminating as to how you’d program the thing. For intuition, I like these two litmus tests. First, parallelism is deterministic or “declarative”: use rayon::prelude::*; fn sum_of_squares (input: &[ i32 ]) -> i32 { input. par_iter () . map (|i| i * i) . sum () } You describe how to split the problem into independent partitions, and implement a function to process one partition at a time . It’s platform’s job to verify the partitioning to be correct (non-racy), process all partitions, and yield control back once that is done. Second, concurrency invariably involves cancelation. Whenever you have two asynchronous computations happening at the same time, there comes a moment when one computation becomes aware that the second computation is no longer necessary, and must be canceled, actively. In general, it is not possible to just wait until the other computation completes: often, the reason why you want to cancel it in the first place is precisely because you’ve learned that it can’t complete (e.g., it is waiting for a message it will never receive). And that is the problem with
Just Use Threads Well, there are more, the chief being that, while you totally can spawn many threads, this often requires system-wide configuration change, which is a non-starter for most application. But absence of cancelation really makes you hit a wall sooner or later. The problem are syscalls. It’s easy enough, in any loopy code, to do something like while ( true ) { if (is_canceled()) return error .Canceld; ... } But, the thread is instead blocked inside the syscall in the kernel, programming language APIs generally doesn’t give any way to unblock it: const read_size = try read(fd, buffer); Wouldn’t it be cool if we could just use standard OS threads, blocking APIs, avoid new shinies like io_uring, but still get to cancel any work reliably? That’s exactly what Zig’s std.Io.Threaded provides.
SIGIO The way this works on POSIX is a bit cursed. Turns out, the kernel actually provides a roundabout way to cancel a blocking syscall — signals. When a thread is blocked in the kernel, and a signal is delivered to the thread, the thread is woken up and the syscall returns EINTR . It is customary to just loop re-try the syscall in such cases, but one doesn’t have to. By itself, signals are not a cancelation mechanism — signaling a thread is inherently racy, the signal might get delivered before the relevant syscall starts, or after it finishes. Conversely, a syscall might get interrupted by signal unrelated to cancelation. The actual protocol is that the canceling thread sets a flag in shared memory to request cancelation, and then signals the cancelee, in a loop, until the cancelation is acknowledged (a different value for a flag in the shared memory). Upon receiving EINTR from a syscall, the thread potentially being canceled checks the value of the flag and either retries the syscall, or acknowledges the cancelation and begins unwinding. See signalCanceledSyscall and, eg fileReadPositionalPosix for the two halves of the protocol. On the user-side, cancelation request is materialized as error.Canceled . Error management as a feature is a combination of cancelation, branching, and reporting, and Zig implements the first two. Cancelation isn’t an error not because it is serendipitous success, but because, vice versa, an error is a cancelation plus a payload. On Windows, there’s a much more direct NtCancelSynchronousIoFile Love the name!. In general, between fibers, IO Completion Ports, Job objects, and this, it seems that NT has a better thought through concurrency story than Unix.