Zig的std.Io.Threaded通过信号和标志机制实现了阻塞系统调用的取消,为并发编程提供了可靠的取消方案
AI 摘要
Zig的std.Io.Threaded通过信号和标志机制实现了阻塞系统调用的取消,为并发编程提供了可靠的取消方案。
推荐理由常规快讯,保留列表
原文
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”:
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
But, the thread is instead blocked inside the syscall in the kernel, programming language APIs generally doesn’t give any way to unblock it:
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 vale 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 verse, 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.
Prior Art
In Java, there’s a similarly looking thread interruption mechanism. Critically, it doesn’t support interrupting syscalls: IOException and InterruptedException are both checked and unrelated, meaning that IOing functions are not interruptible. In Zig, reader and writer interfaces completely type erase errors and therefore support cancelation, though this requires some extra care to handle 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 language-level cancelation (try, defer) which makes post-cancelation cleanup cumbersome and slow. More generally, a lot of angst around concurrency steams from a fact that it falls exactly into the twilight zone between the kernel, the runtime, and the language. There’s almost (interrupts excepted) no concurrency on the CPU, it’s an illusion with a mixed authorship. The language is usually the better equipped one to tackle the problem, but, traditionally, it is handled by the kernel and libc, with adverse effects on language design.
Another problem with pthread_cancel is that it tears down the entire thread, which would be an OK thing to do if threads were cheap. However, creating threads is still slow, and the configured system limit for a number of threads is typically low, so its usually a good idea to pool OS threads. Zig’s Io solves this problem ingeniously, separating, at the interface level, “may run concurrently” from “must run concurrently”:
https://kristoff.it/blog/asynchrony-is-not-concurrency/
This achieves an effect similar to that of std::launch policy (item 36 in effective modern C++, if you have that around). By naming what is happening (io.async vs io.concurrent), Zig makes it easier to understand what is actually going on, and also gets more precise signatures (concurrent is always fallible, async never is). Of course concurrent is backed by a thread pool, falling back on spawning a fresh thread only when the pool is exhausted.
讨论
暂无评论。