Zig's Io.Threaded Is Neat

surprisetalk1 pts0 comments

Zig's Io.Threaded is Neat

std.Io.Threaded<br>is one of the implementations of Zig’s new Io interface that enables concurrency. This is a boring<br>“just use threads” impl. I personally find it neat though — it does this weird thing that<br>I wanted to do for ages, that to my knowledge no<br>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<br>the same thing as state transducers? Yes, obviously, but not really illuminating as to how you’d<br>program the thing.

For intuition, I like these two litmus tests. First, parallelism is deterministic or<br>“declarative”:

use rayon::prelude::*;<br>fn sum_of_squares(input: &[i32]) -> i32 {<br>input.par_iter()<br>.map(|i| i * i)<br>.sum()

You describe how to split the problem into independent partitions, and implement a function to<br>process one partition at a time . It’s platform’s job to verify the partitioning to be correct<br>(non-racy), process all partitions, and yield control back once that is done.

Second, concurrency invariably involves cancelation. Whenever you have two asynchronous<br>computations happening at the same time, there comes a moment when one computation becomes aware<br>that the second computation is no longer necessary, and must be canceled, actively. In general, it<br>is not possible to just wait until the other computation completes: often, the reason why you want<br>to cancel it in the first place is precisely because you’ve learned that it can’t complete (e.g.,<br>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<br>requires system-wide configuration change, which is a non-starter for most application. But absence<br>of cancelation really makes you hit a wall sooner or later. The problem are syscalls. It’s easy<br>enough, in any loopy code, to do something like

while (true) {<br>if (is_canceled()) return error.Canceld; /// Easy!<br>...

But, the thread is instead blocked inside the syscall in the kernel, programming language APIs<br>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<br>io_uring, but still get to cancel any work reliably? That’s exactly what Zig’s std.Io.Threaded<br>provides.

SIGIO

The way this works on POSIX is a bit cursed. Turns out, the kernel actually provides a roundabout<br>way to cancel a blocking syscall — signals. When a thread is blocked in the kernel, and a signal<br>is delivered to the thread, the thread is woken up and the syscall returns EINTR. It is customary<br>to just<br>loop re-try the syscall<br>in such cases, but one doesn’t have to.

By itself, signals are not a cancelation mechanism — signaling a thread is inherently racy, the<br>signal might get delivered before the relevant syscall starts, or after it finishes. Conversely, a<br>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<br>cancelation, and then signals the cancelee, in a loop, until the cancelation is acknowledged (a<br>different value for a flag in the shared memory). Upon receiving EINTR from a syscall, the thread<br>potentially being canceled checks the vale of the flag and either retries the syscall, or<br>acknowledges the cancelation and begins unwinding. See<br>signalCanceledSyscall<br>and, eg<br>fileReadPositionalPosix<br>for the two halves of the protocol.

On the user-side, cancelation request is materialized as error.Canceled. Error management as a<br>feature is a combination of cancelation,<br>branching, and reporting,<br>and Zig implements the first two. Cancelation isn’t an error not because it is<br>serendipitous success, but<br>because, vice verse, an error is a cancelation plus a payload.

On Windows, there’s a much more direct<br>NtCancelSynchronousIoFile<br>Love the name!. In general, between fibers, IO Completion Ports, Job objects, and this, it seems<br>that NT has a better thought through concurrency story than Unix.

Prior Art

In Java, there’s a similarly looking thread interruption mechanism. Critically, it doesn’t support<br>interrupting syscalls: IOException and InterruptedException are both checked and unrelated,<br>meaning that IOing functions are not interruptible. In Zig, reader and writer interfaces completely<br>type erase errors and therefore support cancelation, though this requires some extra care to handle<br>correctly, on top of the usual don’t forget to flush.

pthread_cancel implements a similar signal+flag machinery. However, it doesn’t integrate with<br>language-level...

cancelation thread syscall concurrency threaded doesn

Related Articles