Zig 的 Io.Threaded 非常精彩
2026年8月6日,std.Io.Threaded 是 Zig 新的 Io 接口的一个实现,它支持并发。这是一个乏味的“只需使用线程”的实现。不过,我个人觉得它很精彩——它做了一件我想做很久的奇怪事,据我所知,没有其他人是以这种方式做的,并且实现得比我想象的要好。Io.Threaded 使用阻塞系统调用,完全支持取消。并发与并行 引用 @tedinski 的话,并发是处理(异步、非确定性)事件。并行是利用硬件资源同时做更多事情。我认为这个定义是正确的,但并没有直接提供有用的直觉。并发就是状态转导器吗?是的,显然,但对于你该如何编程并没有真正的启发。为了帮助理解,我喜欢这两个试金石。首先,并行是确定性的或“声明式”的:use rayon::prelude::*; fn sum_of_squares(input: &[i32]) -> i32 { input.par_iter().map(|i| i * i).sum() } 你描述了如何将问题拆分成独立的部分,并定义了一个逐个处理部分的函数。它的平台负责验证分割是否正确(无竞争),处理所有部分,并在完成后将控制权返回。其次,并发无可避免地涉及取消。每当你同时进行两个异步计算时,总会出现一种情况,当一个计算意识到第二个计算不再必要并必须主动取消。在一般情况下,无法仅仅等待另一个计算完成:通常,你想要取消它的原因正是因为你已经了解到它无法完成(例如,它在等待一条永远不会收到的消息)。这就是“只用线程”的问题。实际上,还有更多问题,主要是尽管你完全可以产生多个线程,但这通常需要系统范围的配置更改,对于大多数应用程序而言,这是个不可行的起点。但是缺乏取消最终会让你早晚遇到困境。问题在于系统调用。在任何循环代码中,做一些像这样的事情是足够简单的:while (true) { if (is_canceled()) return error.Canceled; // 简单!... } 但是,线程却被阻塞在内核中的系统调用中,编程语言的 API 通常没有提供任何解除阻塞的方法:const read_size = try read(fd, buffer); // ??? 如果我们能使用标准的操作系统线程,阻塞 API,避免像 io_uring 这样的新鲜事物,但仍然能可靠地取消任何工作,那不是太酷了吗?这正是 Zig 的 std.Io.Threaded 所提供的功能。SIGIO 这在 POSIX 上的工作方式有些诡异。事实证明,内核实际上提供了一种曲折的方式来取消阻塞的系统调用——信号。当一个线程被内核阻塞时,如果信号传递给该线程,线程会被唤醒并且系统调用会返回 EINTR。在这种情况下,通常会简单地循环重试系统调用,但不必如此。信号本身并不是取消机制——向线程发送信号本质上是竞争条件,信号可能在相关的系统调用开始之前或之后被传递。反过来,一个系统调用可能会被与取消无关的信号中断。实际的协议是,取消线程在共享内存中设置一个标志以请求取消,然后循环发送信号给被取消者,直到取消被确认(共享内存中标志的不同值)。在收到系统调用的 EINTR 后,可能被取消的线程检查标志的值,并选择重试系统调用或确认取消并开始撤回。参见 signalCanceledSyscall 和例如 fileReadPositionalPosix 的协议的两个部分。在用户侧,取消请求以 error.Canceled 形式体现。作为一项功能的错误管理是取消、分支和报告的组合,而 Zig 实现了前两者。取消并不是一种错误,不是因为这是一次偶然的成功,而是因为,反之,一个错误就是一个取消加上有效负载。在 Windows 上,有一种更加直接的 NtCancelSynchronousIoFile (爱这个名字!)。一般来说,在纤程、IO 完成端口、作业对象等之间,NT 的并发故事似乎比 Unix 更有深度的思考。先前的技术 在 Java 中,有类似的线程中断机制。重要的是,它不支持中断系统调用:IOException 和 InterruptedException 都是检查的且不相关的,意味着 IO 函数是不可中断的。在 Zig 中,读取器和写入器接口完全类型擦除错误,因此支持取消,尽管这需要额外的注意以正确处理,以及通常不要忘记刷新。pthread_cancel 实现了类似的信号+标志机制。然而,它并没有与语言级别的取消(try,de)集成。
本站免费、广告极少。如果觉得有帮助,可以请我们喝杯咖啡 —— 任何金额都对持续运营有实际帮助。
☕请我喝杯咖啡