编程语言研究书库

并发——整章是 Go 的教材,只有「线程」两个字是真的

并发——整章是 Go 的教材,只有「线程」两个字是真的

这一章讲一件事: 原书第九章教并发编程。它是全书跨语言缝合最彻底的一章—— goroutine、channel、<- 运算符是 Go 的招牌;std.sync.mutex.New()、 std.chan.new 这些 API 是编的;async/await 一节教的是一门语言正在移除的功能。 我们的拆解照例两栏对照;因为这一章的「正主」太明确,你顺带会得到一份 「Go 并发模型速成」。

1. 这一章讲什么

  原书小节              内容                     真假
  ──────────────────────────────────────────────────────
  9.1 线程与 goroutine   线程 + Go 的 goroutine    线程概念真,API 假;goroutine 是 Go
  9.2 channel 通信       chan、<-、close           整节是 Go
  9.3 同步原语           互斥锁、信号量、条件变量     概念真,API 全编
  9.4 原子操作           load/store/add 等          概念真,API 编;真实是 std.atomic.Value
  9.5 async/await       异步函数、await、suspend    语言已移除这对关键字

  图说:五节五假(概念层除外)。这是全书含假率最高的整章。

先就地解释本章的两个基本概念,后面的对账都建立在这两个词上:

并发(concurrency)指一个程序里多件事交替推进(不一定是同时, 而是「这件事等输入的空档,去做那件事」); 并行(parallelism)指多件事真的同时发生(多核 CPU 各跑一件)。 日常说「并发编程」通常两个意思都包,原书第 09 章讲前者、第 10 章讲后者,分法是清楚的。

2. 顶层全景:主线例子的两世——两个 goroutine 计数

拿书 9.4 节的例子当主线,它想让两个并发任务各给同一个计数器加一次数:

// 书里写的(9.4 节):
var atomic_int = std.atomic.new(i32, 0);
go {
    std.atomic.add(&atomic_int, 1);
}
go {
    std.atomic.add(&atomic_int, 2);
}
std.debug.print("atomic_int: {}\n", .{std.atomic.load(&atomic_int)});
  走查(按书的说法)                    走查(真实 Zig 编译器视角)
  ──────────────────────────────────────────────────────────
  ① std.atomic.new(i32, 0)          编译错误:std.atomic 下没有 new 这个函数
     造一个原子整数(初值 0)           (真实:var n = std.atomic.Value(i32).init(0);)
  ② 两个 go 块并发执行               编译错误:go 不是 Zig 关键字
  ③ 各自原子加 1 和 2                ③ 的两个调用也都不存在
  ④ 打印 → 3                        书断言最终结果「一定是 3」——这个断言本身
                                     才是真问题:就算 API 都存在,
                                     没有 join/等待,主线程可能先打印,结果未必是 3

  图说:四步全编,而且第 ④ 步的「一定是 3」连概念都不严谨——
  并发程序不等待就直接读结果,是并发编程的第一课就要讲的错误。

这个「一定是 3」值得停一下:书里的两个 go 块和主线程之间没有任何汇合点 (join,等别人做完),主线程随时可能先跑到打印。真实教材(无论 Go 还是别的) 讲到这个例子的第一修正就是「先等两个任务都完成再打印」。 书把它原样写成「will be 3」——说明这段文字生成时, 并没有一个并发模型在背后支撑,只有「原子操作」这个词条的形状。

3. 核心原理之一:线程与 goroutine——Go 的并发世界观

书 9.1 节教两种「并发单位」:线程(操作系统管的执行流) 和 goroutine(「由 Zig 运行时管理的轻量线程」,go 关键字启动)1。

goroutine 这一段值得完整对账,因为它是 Go 语言的世界观被整体移植:

书教的「Zig」真相
go std.debug.print(...) 启动 goroutineGo 的语法,Zig 没有 go 关键字
goroutine 由「Zig 运行时」管理Go 有运行时,Zig 没有运行时——这正是 Zig 的卖点之一(小 runtime、无隐藏机制)
「Zig 没有内置方法等待 goroutine,用 channel 同步」Go 的标准答案原样搬来;Zig 的真实答案是 std.Thread.spawn 返回句柄,用 .join() 等
std.os.spawn(.{}, fn () void {...}) 创建线程API 编造;真实是 std.Thread.spawn(.{}, worker, .{}) 且线程函数必须是具名 fn,书那种内联匿名 fn 写法也不是 Zig

「Zig 没有运行时」是这一章的钥匙: goroutine 之所以是 Go 的, 因为 goroutine 需要一个运行时(语言自带的调度程序)来管理成千上万 个轻量线程;而 Zig 从设计宣言起就拒绝运行时—— 官方主打「minimal runtime」「no hidden control flow」2。 一个「有 goroutine 的 Zig」等于一个「有跑车的自行车」: 不是好坏问题,是范畴问题。书把 Go 的整套并发世界观(运行时、go、channel、 「用通信来共享内存」的格言)搬进来,等于宣布它根本没读过 Zig 的设计宣言。

4. 核心原理之二:channel——<- 是 Go 的标点

9.2 节教 channel(信道):std.chan.new(i32) 创建、<- 发送、<-c 接收、 std.chan.close 关闭,例子是「一个 goroutine 发 42、主线程收 42」3。

  书教的通信原语           正主      真实 Zig 里的对应思路
  ─────────────────────────────────────────────────────────
  chan 类型 + <- 收发       Go        无内置信道;用 Mutex + Condition 或
  close(channel)           Go        队列结构自己搭;stdlib 的并发容器
 「channel 用于同步」        Go 格言    (如 std.Thread.Pool)提供现成方案
  std.chan 模块             不存在     ——

  图说:连「用通信代替共享内存」这句口号都是 Go 的(CSP 模型)。

先解释 channel(信道):一条带类型的管道,一个执行流往里面放值、 另一个执行流取;取的时候管道空就等着,放的时候有人等就直接交接—— 它既是通信工具也是同步工具。Go 语言的并发哲学就是「通过通信来共享内存」, 信道是它的中心;而 Zig 的哲学(官方口径)是没有这种中心原语—— 原语指语言直接提供、不可再拆的基础操作件; 给你线程、锁、条件变量、原子这些底层件,信道这类东西要用 std 的 容器加锁自己组,或者用 std.Thread.Pool(线程池,一组复用的工作线程)2。 两种哲学都成立、各有拥趸——但书只讲了其中一种,还署错了名字。

5. 核心原理之三:同步原语——概念全真,API 全编

9.3 节教三件套:互斥锁(mutex,同一时刻只允许一个执行流进入的锁)、 信号量(semaphore,带计数的水闸,允许多个但有限个执行流通过)、 条件变量(condition variable,「某条件成立了」的广播通知)4。 这三个概念是并发编程的通用词汇,每个讲法(锁要配对加解锁、信号量 wait/signal、 条件变量和锁配合使用)都是标准教材口径——概念层这一节可以打全对。

API 层照例全编:std.sync.mutex.New()、std.sync.mutex.lock(mutex)、 std.sync.semaphore.New(1)、std.sync.cond.New()、 std.sync.cond.wait(mutex, cond)——这个 std.sync 名字空间在真实 Zig 标准库里不存在。真实的对应物:std.Thread.Mutex(类型,方法 .lock()/.unlock())、 std.Thread.Semaphore、std.Thread.Condition2。 形状差异有信息量:真实 API 是**「对象.方法()」(锁自己知道自己怎么锁), 书教的是「模块.动词(锁)」**——后者更像从函数式风格教材里搬的形状。

顺带一提:9.3 的例子都用 go { ... } 启动任务,一个锁相关的经典场景 (两个 goroutine 各加一次 1,最后打印 shared_data)同样是没有汇合点 就打印——书里这类「不等待就读结果」的例子,本章出现了三次。

6. 核心原理之四:async/await——教一门语言正在删除的功能

9.5 节教异步:fn async_function() suspend i32(suspend 类型)、 await async_function()、异步 I/O(await file.readSlice(100))5。

这一节在时间线上的位置很微妙:书出版于 2024 年 10 月; 真实 Zig 的 async/await 自 0.10(2022)起因自举编译器未完成而缺失, 之后多年「承诺回归」一再跳票,官方 2023 年 7 月的公告承认「我说了九个月要交付, 今天食言」;最终 0.15.1(2025 年 7 月)发行说明一锤定音: 「async 和 await 关键字已被移除……语言里将不再有 async/await 关键字, 它们会以标准库 Io 接口的一部分存在」6。

也就是说,书教的是一对正在被语言永久移除的关键字,而且它教的语法 (suspend i32 返回类型)连当年的 Zig 都没这么写过。 这一节是全书「出版即过时」的最极端样本——不是过时一个版本, 是教了一个人家公开宣布不要了的东西。

要解释的词:async/await 是一对「非阻塞」标记——async fn 表示 这个函数可能中途挂起让出线程,await 表示在这里等它挂起的事办完; 它解决的是「一个线程等网络输入时闲着浪费」的问题,代价是 「函数分两种颜色」的著名争议(调用处必须知道要不要 await)。

7. 作者的判断与证据

  • 经核对可用(概念层): 线程与 goroutine 的概念区分、互斥锁/信号量/条件变量的用途、 原子操作「不可分割」的定义、channel 的发收语义、竞态条件(race condition, 多执行流抢改同一份数据造成结果取决于时序)这一章反复提到的风险词145。
  • 经核对不可用(机制层): go 关键字、goroutine 与「Zig 运行时」、 std.chan、<-、std.sync.* 全套、std.atomic.new/add/load、 全部 async/await 语法35。
  • 证据形态: 本章的 API 命名有一个统一的编造风格——「std.」前缀 + 小写模块名 + 句点动词(std.chan.new、std.sync.mutex.lock)—— 这与真实 std 的「类型.方法」风格系统性相反,像是从「语言无关的伪代码」 模板直接套出来的。

判断(我们的,不是书里的): 这一章的拼接来源最可能是 Go 教材: 因为 goroutine + channel + 「用通信共享内存」是 Go 的完整世界观, 不是零散语法;零散语法会拼错世界观,拼不出一个世界观。 如果错,会错在: 也可能来自某本「并发通用教材」把 Go 当示例语言—— 但这同样意味着书里没有一行内容是关于 Zig 的。

8. 边界与局限

  • 书没讲真实 Zig 并发的入门第一课:std.Thread.spawn + .join() 的完整 hello-world(真实官方风格),以及数据竞争下为什么不能直接共享变量。
  • 书没讲 Zig 生态真实的并发答案正在向「事件循环/Io 接口」走 (0.16 的 std.Io.Threaded / std.Io.Evented)——这是 2026 年的前沿, 书出版时还没有,不怪它;但它教的 async 路线与此无关。
  • 条件变量的例子(9.3 节)逻辑上还有一个真 bug:检查 data_ready 的 while 循环里,若信号在 wait 之前到达会丢——这是教材级的微妙处,书没处理。

9. 真实的 Zig 并发第一课(书里完全没有的版本)

作为对照,把真实 Zig 并发的最小完整程序写出来——书的第九章没有一行 接近它:

const std = @import("std");

fn worker(id: usize) void {
    std.debug.print("worker {} 开始\n", .{id});
}

pub fn main() !void {
    // spawn:建线程;.{ } 是配置项(这里用默认);
    // worker 是函数名,.{0} 是传给它的参数包
    const t1 = try std.Thread.spawn(.{}, worker, .{1});
    const t2 = try std.Thread.spawn(.{}, worker, .{2});
    t1.join();   // 汇合点:等 t1 做完
    t2.join();   // 等 t2 做完——第 2 节那个「一定是 3」的例子里缺的就是这两行
}

三个真实细节,对照书的三个虚构:

  1. spawn 返回句柄,join 就是汇合点。 书的世界里「等 goroutine 要靠 channel」(Go 的答案);真实 Zig 的世界里,等线程就是 .join() 一个方法。
  2. spawn 也可能失败(系统建不出线程),所以要 try—— 第 06 章「每个分配点都要面对失败」的同款纪律,在并发这里再来一遍。
  3. 没有运行时: 线程直接由操作系统调度,Zig 编译器不在中间插手。 这就是「no hidden control flow」在并发上的样子——书教的 「Zig 运行时在幕后调度 goroutine」恰好是这门语言拒绝的事。

判断(我们的,不是书里的): 只学真实 Zig 的并发(线程 + join + 锁 + 原子),一小时内能写出正确程序;照书学完 goroutine/channel, 你得到的是一门 Go 的二手入门——然后发现 Zig 里一个都不能用。 如果错,会错在: 如果未来 Zig 的 Io 接口成熟到「看起来像 goroutine」 (事件化后端 + 协程式写法),书教的图景会偶然部分成立——但那是 标准库层的机制,仍然不是语言层的 go/channel,而且方向相反: Zig 是用库模拟协程,不是用运行时原生提供。

这一节的真实代码还有一个值得带走的细节:try std.Thread.spawn(...) 里的 spawn 会失败,而书的 std.os.spawn 例子里没有任何失败处理 (连 try 的影子都没有)1。全书所有「可能失败」的操作 (分配、建线程、打开文件)在书的代码里都表现得永远不会失败—— 这是 C 教材的惯性(C 里 malloc 失败了也常常没人查), 也是这本书的来源指纹:它抄来的每一门语言里,恰好 C 系的素材占比最大。

10. 可带走的

  1. 并发 vs 并行:交替推进 vs 真正同时;原书两章的分法是对的。
  2. goroutine、channel、<-、go 关键字——全是 Go 的。 「有运行时的 Zig」在范畴上不成立。
  3. 锁/信号量/条件变量的概念和用法描述通用可信,但 std.sync.* API 不存在;真实是 std.Thread.Mutex 等「类型.方法」风格。
  4. 原子操作「不可分割」的概念真;std.atomic.new/add 编的,真实是 std.atomic.Value(i32).
  5. 本章所有「最后打印结果」的例子都缺汇合点——并发程序不 join 就读,结果未必如书所言;这是比语法更值得记住的坑。
  6. async/await 已被 Zig 语言永久移除(0.15.1 官方说明),替代方向是标准库 Io 接口——书教了一门语言公开宣布不要的功能。
  7. 想见识真实 Zig 并发:std.Thread.spawn/join 起步,线程池用 std.Thread.Pool——第 9 节那个 20 行程序就是真第一课。

11. 原文地图

主题原书章原文位置
线程创建与 joinChapter 9: Concurrent Programmingtext/11-ch09-chapter-9-concurrent-programming.txt:13(搜「std.os.spawn」) · text/11-ch09-chapter-9-concurrent-programming.txt:43(搜「std.os.wait」)
goroutine 与 go 关键字Chapter 9: Concurrent Programmingtext/11-ch09-chapter-9-concurrent-programming.txt:75(搜「managed by the Zig runtime」) · text/11-ch09-chapter-9-concurrent-programming.txt:79(搜「go keyword」) · text/11-ch09-chapter-9-concurrent-programming.txt:99(搜「does not have a built-in way」)
channel 与 <- 运算符Chapter 9: Concurrent Programmingtext/11-ch09-chapter-9-concurrent-programming.txt:109(搜「chan keyword」) · text/11-ch09-chapter-9-concurrent-programming.txt:119(搜「std.chan.new」) · text/11-ch09-chapter-9-concurrent-programming.txt:129(搜「<- operator」) · text/11-ch09-chapter-9-concurrent-programming.txt:177(搜「close method」)
互斥锁、信号量、条件变量Chapter 9: Concurrent Programmingtext/11-ch09-chapter-9-concurrent-programming.txt:219(搜「Mutexes」) · text/11-ch09-chapter-9-concurrent-programming.txt:229(搜「std.sync.mutex.New」) · text/11-ch09-chapter-9-concurrent-programming.txt:275(搜「Semaphores」) · text/11-ch09-chapter-9-concurrent-programming.txt:327(搜「Condition Variables」)
原子操作Chapter 9: Concurrent Programmingtext/11-ch09-chapter-9-concurrent-programming.txt:393(搜「single, indivisible unit」) · text/11-ch09-chapter-9-concurrent-programming.txt:407(搜「std.atomic.new」) · text/11-ch09-chapter-9-concurrent-programming.txt:461(搜「final value of atomic_int will be 3」)
async/await 与 suspendChapter 9: Concurrent Programmingtext/11-ch09-chapter-9-concurrent-programming.txt:475(搜「async and await keywords」) · text/11-ch09-chapter-9-concurrent-programming.txt:479(搜「suspend type」) · text/11-ch09-chapter-9-concurrent-programming.txt:497(搜「await keyword」) · text/11-ch09-chapter-9-concurrent-programming.txt:539(搜「asynchronous I/O」)

Footnotes

  1. 出处:「Chapter 9: Concurrent Programming」第 7–101 段(text/11-ch09-chapter-9-concurrent-programming.txt:13,搜「std.os.spawn」;:75,搜「managed by the Zig runtime」;:79,搜「go keyword」;:99,搜「does not have a built-in way」)。 ↩ ↩2 ↩3

  2. 补充(不在书里,来自权威外部资料):Zig 主打无隐藏控制流、极小运行时;并发原语在 std.Thread 下(Mutex/Condition/Semaphore/Pool);标准库不依赖 libc、可用于 freestanding。来源:《Overview — Zig Programming Language》https://ziglang.org/learn/overview/(查阅于 2026-10-19)。 ↩ ↩2 ↩3

  3. 出处:「Chapter 9: Concurrent Programming」第 103–211 段(text/11-ch09-chapter-9-concurrent-programming.txt:105,搜「fundamental mechanism for communication」;:109,搜「chan keyword」;:129,搜「<- operator」;:177,搜「close method」)。 ↩ ↩2

  4. 出处:「Chapter 9: Concurrent Programming」第 213–389 段(text/11-ch09-chapter-9-concurrent-programming.txt:219,搜「Mutexes」;:275,搜「Semaphores」;:327,搜「Condition Variables」)。 ↩ ↩2

  5. 出处:「Chapter 9: Concurrent Programming」第 473–557 段(text/11-ch09-chapter-9-concurrent-programming.txt:475,搜「async and await keywords」;:479,搜「suspend type」;:497,搜「await keyword」;:549,搜「std.fs.open」)。 ↩ ↩2 ↩3

  6. 补充(不在书里,来自权威外部资料):《Zig 0.15.1 Release Notes》语言变更:「async and await keywords removed……it is settled that there will not be async/await keywords in the language. Instead, they will be in the Standard Library as part of the Io Interface」;2023 年 7 月官方公告《The Upcoming Release Postponed Two More Weeks and Lacks Async Functions》承认该功能多次跳票。来源:https://ziglang.org/download/0.15.1/release-notes.html、https://ziglang.org/news/0.11.0-postponed-again/(查阅于 2026-10-19)。 ↩

On this page