编程语言研究书库

第 9 章拆解(上)— 异步 I/O 的模型与语法

异步 I/O 的模型与语法

这一半讲什么: 异步 I/O 要先拆两个被混用的词——异步和并发不是一回事; 再看事件循环为什么能用一个线程盯住几千个连接;然后是 Zig 0.16 的异步语法全家桶: io.async 发射、await 收割、io.concurrent 强制真并行、Io.Select 抢第一名。 下一半(第 10 篇)用这些积木搭一个真实的 TCP 代理,并下到内核看 io_uring 和 kqueue 的raw形态。

主线例子:扇出模式。三个任务各睡 300/400/500 毫秒,顺序执行要 1.2 秒,扇出后 wall time 只有约 600 毫秒——慢的那个说了算。

1. 顶层全景:同步模型的账,和事件循环的还法

  同步世界:            异步世界:
  每连接一线程         一个事件循环
  ┌────┐ ┌────┐       ┌─────────────────────┐
  │连接1│ │连接2│ …     │ while 真:            │
  └─┬──┘ └─┬──┘       │   问内核:谁就绪了?    │
    阻塞中  阻塞中       │   就绪的 → 续跑它的协程 │
  每个线程一份栈        └─────────────────────┘
  (2MB/8MB)           一个线程,无数协程
  上下文切换贵          协程挂起只是记下"从哪续"

  图说:同步模型里「等待」的成本是线程;异步模型里「等待」的成本只是一个"待续点"。

同步模型里,每次 I/O(读盘、等网络包)都阻塞调用线程,操作系统只能切到别的线程让 CPU 别闲着——保存恢复寄存器、内核记账,连接数上到几千时,线程栈的内存和上下文切换的 CPU 就成了主瓶颈1。异步的做法:单线程发起多个 I/O,在硬件和内核干活时继续执行别的逻辑2。

io_uring(Linux)和 kqueue(BSD/macOS)是内核给的两种答案:应用把 I/O 请求放进共享内存的提交队列,内核在后台异步处理,完成后把结果放进完成队列——昂贵的系统调用次数被压到最低,内核批处理,只在数据就绪时才叫醒你3。

2. 异步不是并发

这两个词经常一起出现,但管的是不同的建筑层面。并发关于结构——把程序拆成能独立执行的单元;异步关于时间——发起一个操作后不等它做完,先去干别的,完成后再回来收结果4。一个程序可以并发而不异步(多线程各干各的同步活),也可以异步而不并发(单线程事件循环)。本章的主角是后一种的组合拳。

3. 非阻塞代码的思维转换

写非阻塞代码,是从「线性流程」到「状态驱动」的思维转换5。同步函数里你 read() 然后原地等数据;非阻塞世界里,read 可能立刻返回一个 WouldBlock 错误——意思是「现在没数据,别傻等,去注册你对这个文件描述符的兴趣,让出控制权」,等事件循环通知「这个描述符可以推进了」再回来从断点续跑6。在 Zig 里,这一层被包成「返回一个 Future(任务句柄)的函数」——CPU 永远在干有意义的活,不被硬件的慢拖住7。

4. async 和 await:不是关键字的语法

Zig 0.16 里 async/await 不是语言关键字,是 io 实例上的方法——这是它们最不寻常的地方8。机制三句话:

  • io.async(函数, .{参数}) 把一个函数发射成一个协程——可以在任何 I/O 边界挂起自己、把线程让给其他协程的工作单元;调用立即返回一个 Future 句柄,不阻塞9。
  • task.await(io) 是说「我需要这个结果才能往下走」——挂起调用者,直到任务完成、交付返回值10。
  • 威力在于:写出来的代码和同步代码几乎一模一样——没有回调、没有显式状态机、没有手工管理的续体对象——而运行时在任何一个协程等操作系统时,可以在单线程上交错推进几十个并发操作11。

一个最小完整生命周期(书上的 basic.zig):发射两个各睡 1 秒的平方计算,中间主线程自己跑一个 100 万次累加的旁路计算,然后两个 await 收结果 42^2=1764, 7^2=49, side-sum=499999500000——两个 1 秒的睡眠互相重叠、也和主线程的活重叠,总墙钟时间只有约 1 秒,不是 2 秒。发射与迟收之间的这个重叠,就是异步编程模型的地基12。每个 Future 配一个 defer task.cancel(io) 安全网:main 提前退出时,还在飞的协程被取消、资源被释放——否则它可能继续往已经不属于它的栈内存里写13。

5. io.concurrent 与扇出模式:要「真并行」时的选择

io.async 让 Io 实现自己决定怎么执行任务——内联、工作线程、协程都行。当多个独立任务必须全部完成才能继续、而且你要它们真的同时跑,用 io.concurrent:它强制并发执行,Io 实现给不了就返回 ConcurrencyUnavailable 错误14。

主线例子,扇出模式(fanOut.zig):三个抓取任务,延迟分别是 300 + id*100 毫秒(300/400/500),各自把结果 1000*id 写进预分配数组里自己专属的那一格,然后三个顺序 await 收齐——输出 Results: 1000 2000 300015。账是这么算的:

顺序执行:300 + 400 + 500 = 1200 ms
扇出执行:≈ 最慢的一个 = 600 ms(输出按发射顺序排,不按完成顺序)

为什么不用锁:每个任务只碰自己的槽格——同一槽并发写才是数据竞争,
不同槽的并发写天然安全(第 8 章「每人一个槽」的无锁设计在这里复现)

图说:这个模式在分布式系统和网络服务里无处不在——同时查多个后端、同时抓多个资源、多个独立计算汇成一个结果,形状都是「一扇出、一收拢」16。await 在源码里顺序书写,但那并不把执行串行化:轮到 t1.await 时,t2、t3 早就在自己的路上跑了;每个 await 只是等那一个任务完17。

6. 写的对称性:背压与半幅图景

异步读只是半幅图景。写操作有自己的麻烦:一个慢速或背压(back-pressure,接收方处理不过来、 TCP 发送缓冲区被填满)的客户端,能让幼稚的服务器卡在「往套接字里 flush 字节」上,把同一线程上的其他协程全部拖住18。std.Io 对读写是对称处理的——无法立即完成的写,和读一样挂起协程、让出线程,事件循环得以继续伺候别的连接,操作系统自己在排空发送缓冲19。

实证(multipleWrites.zig):两个互不依赖的文件写,各起一个 io.async,两个都在 await 之前发出——两个写交叠执行,完成顺序由 I/O 子系统决定,Both files written successfully! 收尾20。

7. Io.Select:只要第一名

Io.Select 等几个并发任务里** whichever 先完成,自动取消输家**21。用法四步:定义一个「每竞争者一个变体」的联合类型;Io.Select(Result).init(io, &buf) 用一块够装所有任务的缓冲建选择器;sel.async(.t1, 函数, .{参数}) 把竞争者逐个发射;sel.await() 阻塞到第一个完成,返回带标签的联合值,switch 分发22。实测:1 秒和 2 秒两个睡眠赛跑,永远输出 Task 1 finished first23。书点明它是超时、后端竞速、一切「最快响应说了算」场景的积木——第 10 篇的代理就用它做「两个方向先断哪个」的拆会话开关24。

8. 作者的判断与证据

  • 判断: 异步模型省的是线程(2 MB 或 8 MB 的栈 × 连接数)和上下文切换,代价是显式的状态记账——同步程序里调用栈替连接记状态,异步里要自己管25(下篇展开)。证据:内存账是机制推演,没有实测内存对比。
  • 判断: 「await 顺序书写 ≠ 执行串行化」17。证据:fanOut 的墙钟时间(约 600 ms 对 1500 ms 之和)。
  • 坦白的边界: sleepFor 示例函数里 catch {} 吞错误只为聚焦 select 机制,生产代码应返回 !void 传播错误26。

9. 边界与局限

  • 协程不是免费的:纯 CPU 密集、从不在 I/O 挂起的函数会把事件循环整个堵住,拖死所有连接——没有运行时救你,责任全在函数设计上(下篇的「事件循环饿死」细讲)27。
  • io.async 的执行方式由 Io 实现决定(内联/线程/协程),移植性意味着不能假设它真并行;要真并行就用 io.concurrent 并接受 ConcurrencyUnavailable 的可能14。
  • 本章所有例子都在 Threaded 默认后端(真实 OS 线程池)上跑;io_uring/kqueue 的事件化后端要显式构造,行为有差异(下篇)28。

10. 可带走的

  1. 同步的账:每连接一线程,连接上千时栈内存 + 上下文切换拖垮;异步的还法:一个事件循环 + 共享内存提交/完成队列。
  2. 异步≠并发:异步管「不等结果先干别的」,并发管「拆成独立单元」。
  3. WouldBlock = 「现在没数据,注册兴趣、让出控制、事件循环叫你回来续」。
  4. 0.16 的 async/await 是 io 的方法:io.async 发射得 Future(不阻塞)、await 收结果(挂起调用者)。
  5. 每个 Future 配 defer task.cancel(io)——main 早退时别让协程往已失效的栈里写。
  6. io.async vs io.concurrent:前者调度器自由选执行方式,后者强制真并行(可能 ConcurrencyUnavailable)。
  7. 扇出模式:结果写各自专属槽格,无锁、无堆分配、无信道;总时间 = 最慢任务,不是求和。
  8. 写和读对称:背压的慢客户端不该堵住其他协程,写不出去就挂起让出。
  9. Io.Select = 竞速 + 自动取消输家,超时的天然积木。

11. 原文地图

主题原书章原文位置
同步模型的瓶颈账Chapter 9: Implementing Asynchronous I/Otext/104-ch09-chapter-9-implementing-asynchronous-i-o.txt:11(搜「context switch」)
异步≠并发Chapter 9: Implementing Asynchronous I/Otext/104-ch09-chapter-9-implementing-asynchronous-i-o.txt:43(搜「structure」)
提交队列/完成队列Event loops: how io_uring and kqueue work under the hoodtext/105-fm-event-loops-how-io-uring-and-kqueue-work-under-t.txt:35(搜「submission queue」)
非阻塞思维转换Writing nonblocking codetext/106-fm-writing-nonblocking-code.txt:5(搜「state-driven」)
WouldBlock 语义Writing nonblocking codetext/106-fm-writing-nonblocking-code.txt:17(搜「WouldBlock」)
协程定义Writing nonblocking codetext/106-fm-writing-nonblocking-code.txt:55(搜「coroutine」)
async/await 是方法Using async and awaittext/107-fm-using-async-and-await.txt:13(搜「not」)
两个 1 秒重叠成 1 秒Using async and awaittext/107-fm-using-async-and-await.txt:150(搜「foundation」)
cancel 防写失效栈Using async and awaittext/107-fm-using-async-and-await.txt:342(搜「no longer belongs」)
concurrent 强制并行Using async and awaittext/107-fm-using-async-and-await.txt:284(搜「forces actual concurrent」)
最慢者定价Using async and awaittext/107-fm-using-async-and-await.txt:366(搜「slowest task」)
背压堵循环Using async and await for write operationstext/108-fm-using-async-and-await-for-write-operations.txt:9(搜「back-pressured」)
写读对称挂起Using async and await for write operationstext/108-fm-using-async-and-await-for-write-operations.txt:15(搜「symmetrically」)
Select 抢第一名Chapter 9: Implementing Asynchronous I/Otext/109-fm-being-interested-in-the-first-result-only.txt:5(搜「whichever」)
超时/竞速积木Chapter 9: Implementing Asynchronous I/Otext/109-fm-being-interested-in-the-first-result-only.txt:100(搜「building block」)

本篇拆解依据原书第 9 章前半(含各小节);代理与内核多路复用部分见下一篇。每条出处可用 node scripts/book-verify.mjs systems-programming-with-zig 回核。

Footnotes

  1. 出处:「Chapter 9: Implementing Asynchronous I/O」第 11 段(text/104-ch09-chapter-9-implementing-asynchronous-i-o.txt:11,搜「context switch」)。 ↩

  2. 出处:「Chapter 9: Implementing Asynchronous I/O」第 17 段(text/104-ch09-chapter-9-implementing-asynchronous-i-o.txt:17,搜「continue executing other logic」)。 ↩

  3. 出处:「Event loops: how io_uring and kqueue work under the hood」第 35 段(text/105-fm-event-loops-how-io-uring-and-kqueue-work-under-t.txt:35,搜「submission queue」)。 ↩

  4. 出处:「Chapter 9: Implementing Asynchronous I/O」第 43 段(text/104-ch09-chapter-9-implementing-asynchronous-i-o.txt:43,搜「structure」)。 ↩

  5. 出处:「Writing nonblocking code」第 5 段(text/106-fm-writing-nonblocking-code.txt:5,搜「state-driven」)。 ↩

  6. 出处:「Writing nonblocking code」第 17 段(text/106-fm-writing-nonblocking-code.txt:17,搜「WouldBlock」)。 ↩

  7. 出处:「Writing nonblocking code」第 25 段(text/106-fm-writing-nonblocking-code.txt:25,搜「meaningful work」)。 ↩

  8. 出处:「Using async and await」第 13 段(text/107-fm-using-async-and-await.txt:13,搜「not」)。 ↩

  9. 出处:「Writing nonblocking code」第 55 段(text/106-fm-writing-nonblocking-code.txt:55,搜「coroutine」)。 ↩

  10. 出处:「Using async and await」第 35 段(text/107-fm-using-async-and-await.txt:35,搜「suspends the caller」)。 ↩

  11. 出处:「Writing nonblocking code」第 71 段(text/106-fm-writing-nonblocking-code.txt:71,搜「reads almost identically」)。 ↩

  12. 出处:「Using async and await」第 150 段(text/107-fm-using-async-and-await.txt:150,搜「foundation」)。 ↩

  13. 出处:「Using async and await」第 342 段(text/107-fm-using-async-and-await.txt:342,搜「no longer belongs」)。 ↩

  14. 出处:「Using async and await」第 284 段(text/107-fm-using-async-and-await.txt:284,搜「forces actual concurrent」)。 ↩ ↩2

  15. 出处:「Using async and await」第 383 段(text/107-fm-using-async-and-await.txt:383,搜「1000 2000 3000」)。 ↩

  16. 出处:「Using async and await」第 262 段(text/107-fm-using-async-and-await.txt:262,搜「fans out」)。 ↩

  17. 出处:「Using async and await」第 352 段(text/107-fm-using-async-and-await.txt:352,搜「does not serialize」)。 ↩ ↩2

  18. 出处:「Using async and await for write operations」第 9 段(text/108-fm-using-async-and-await-for-write-operations.txt:9,搜「back-pressured」)。 ↩

  19. 出处:「Using async and await for write operations」第 15 段(text/108-fm-using-async-and-await-for-write-operations.txt:15,搜「symmetrically」)。 ↩

  20. 出处:「Using async and await for write operations」第 134 段(text/108-fm-using-async-and-await-for-write-operations.txt:134,搜「overlap」)。 ↩

  21. 出处:「Chapter 9: Implementing Asynchronous I/O」第 5 段(text/109-fm-being-interested-in-the-first-result-only.txt:5,搜「whichever」)。 ↩

  22. 出处:「Chapter 9: Implementing Asynchronous I/O」第 54 段(text/109-fm-being-interested-in-the-first-result-only.txt:54,搜「backing buffer」)。 ↩

  23. 出处:「Chapter 9: Implementing Asynchronous I/O」第 109 段(text/109-fm-being-interested-in-the-first-result-only.txt:109,搜「Task 1 finished first」)。 ↩

  24. 出处:「Chapter 9: Implementing Asynchronous I/O」第 100 段(text/109-fm-being-interested-in-the-first-result-only.txt:100,搜「building block」)。 ↩

  25. 出处:「Chapter 9: Implementing Asynchronous I/O」第 173 段(text/109-fm-being-interested-in-the-first-result-only.txt:173,搜「explicitly manage」)。 ↩

  26. 出处:「Chapter 9: Implementing Asynchronous I/O」第 92 段(text/109-fm-being-interested-in-the-first-result-only.txt:92,搜「production code」)。 ↩

  27. 出处:「Chapter 9: Implementing Asynchronous I/O」第 177 段(text/109-fm-being-interested-in-the-first-result-only.txt:177,搜「starvation」)。 ↩

  28. 出处:「Chapter 9: Implementing Asynchronous I/O」第 1070 段(text/109-fm-being-interested-in-the-first-result-only.txt:1070,搜「Threaded」)。 ↩

On this page