goroutine、channel 与 select:Go 的并发模型,连同它的两种经典事故¶
这一章回答四个问题: 怎么开一条并发执行流(goroutine)? 两条流之间怎么安全地传数据(channel)?怎么同时等好几件事(select、WaitGroup)? 以及——并发程序会出哪两种经典事故(数据竞争、死锁),怎么自己把它们造出来再修好? 读完你会拿到 Go 并发模型的完整心智图,外加「race 是这样产生的、锁是这样挡的」的亲手经验。
1. 这一章在原书里的位置¶
原书第 3 章(Go Concurrency)是语言部分的压轴。第一部分开篇说过一个立场, 这一章是它的兑现:并发在 Go 里不是优化手段,而是思维模型1。 书里那句口号值得现在记住,后面全章都是它的展开: 「不要通过共享内存来通信,要通过通信来共享内存。」2
2. 顶层全景¶
① 执行单元 goroutine → go 关键字;调度归运行时,不归操作系统(§3)
② 等待机制 WaitGroup → Add/Done/Wait 计数;Go 1.25 起有 wg.Go(§4)
③ 沟通管道 channel → 无缓冲=当面交接;缓冲=信箱;关频道有规矩(§5–§6)
④ 多路监听 select → 哪个 channel 先就绪走哪个;配 time.After 做超时(§7)
⑤ 两个事故 race/deadlock → 亲手制造、工具抓获、设计根治(§8–§11)
图说:五个部件一张图。①是「谁干活」,③④是「怎么配合」,⑤是「配合坏了什么样」。
3. goroutine:开一条执行流,只需要一个 go¶
goroutine 是 Go 里独立的执行流。 它不是什么重型的「线程」:
你在函数调用前加一个 go 关键字,这个调用就被调度到一条 goroutine 上异步跑,
当前代码不等它3。背后的事(先给效果,细节在第 4 章调度器一节):
运行时管理一个线程池,用「偷工作」的调度算法把成千上万个 goroutine 复用到少量
操作系统线程上;同一时刻在跑 Go 代码的线程数上限由 GOMAXPROCS 决定
(默认值是机器的逻辑 CPU 数),阻塞在系统调用上的 goroutine 不占这个名额4。
每个 Go 程序至少有一条 goroutine——跑 main() 的那条。
想让程序整体退出要用 os.Exit();goroutine 自己结束用 return 就够了5。
第一个怪现象:main 不等任何人¶
书上第一个程序就故意留下一个坑:main 里循环 go worker(i) 开出 20 条 goroutine,
没有任何等待机制,然后 main 直接结束。输出什么样?6
$ go run ch03/goroutines.go 20
Starting 20 goroutines...
Worker 3 starting
Main function is exiting without waiting!
Worker 20 starting
Worker 1 starting
(然后程序就没了——剩下 17 条 goroutine 根本没轮到执行)
图说:主函数一退出,整个进程(连同所有 goroutine)立刻蒸发。 输出的顺序每次还不一样——goroutine 何时被调度,你说了不算。
这个例子还带一个语言级的坑(书里用 Note 专门警告):
goroutine 按引用捕获循环变量,不按值拷一份。 循环里 go func(){ 用 i }()
拿到的 i 是循环那个变量的内存位置,等 goroutine 真跑起来时循环可能已经走到后面了——
经典症状是「所有 goroutine 打印同一个数」。修法曾经是循环开头写 i := i 造一个局部副本;
Go 1.22 起编译器自动为每轮迭代造新变量,这个坑被官方填了7。
什么时候该开 goroutine:幂等性判据¶
书里给了一个不常见但好用的判据:一个操作如果做多次结果一样(幂等,idempotent), 它就是 goroutine 的好候选——因为幂等意味着不依赖执行过程中会变化的外部状态8。 反过来,如果一条 goroutine 的结果依赖某个全局变量此刻的值,它产出的就是非确定性。
4. sync.WaitGroup:等大家都干完的计数器¶
解决「main 不等人」的标准工具是 sync.WaitGroup,一个计数器,三个方法:
var wg sync.WaitGroup
for _, t := range tasks { // tasks = task1, task2, task3
wg.Add(1) // ① 计数 +1
go func(task string) {
defer wg.Done() // ② 干完计数 −1;defer 保证任何退出路径都执行
fmt.Println("Processing:", task)
}(t)
}
wg.Wait() // ③ 计数不归零就一直等
fmt.Println("All tasks completed!") // 这句永远最后打印
跑出来的顺序可能每次不同(Processing: task2 在 task1 前面),
但 All tasks completed! 永远最后——这就是 WaitGroup 的合同9。
两个纪律:每个 Add() 必须有配对的 Done();Add 多了,Wait() 永等(死锁的一种);
Done 多了,计数变负,直接 panic「negative WaitGroup counter」10。
Go 1.25 加了 wg.Go(func(){...}):把「+1、开 goroutine、defer Done」三步合一,
少掉忘写 Done 的整个 bug 类别11。本章后面的 worker pool 和 monitor 例子都用了它。
5. channel:goroutine 之间的管道¶
goroutine 之间不能直接通信,共享数据只有两条路:channel,或者(尽量避免的)带锁的全局变量12。 channel 两个要素:类型(只能传这一种值)和缓冲(能暂存几个值)。
- 无缓冲
ch := make(chan int):每一次发送都要等一个接收,反之亦然——当面交接, 双方必须同时就位。这是严格的同步点。 - 缓冲
ch := make(chan int, 3):发送方最多领先 3 个值不等人,第 4 个开始才阻塞; 接收方只在空了的时候阻塞。
书上 bufUnbuf.go 的输出把这个差别演得极清楚(发送每个值花 200ms,接收花 400ms)13:
无缓冲:Sending 1... Sent 1! Received 1 Sending 2... ← 严格交替,发送必须等接收
缓冲: Sending 1... Sent 1! Received 1 Sending 2... Sent 2!
Received 2 Sending 3... Sent 3! Sending 4... Sent 4! ← 发送连跑4个不等
Received 3 ← 直到缓冲塞满,才再次出现等待
图说:无缓冲是「手递手」,缓冲是「信箱」。缓冲大小的选择就是「生产方能领先消费方多少」的选择, 这也是背压(backpressure)的最原始形态。
关闭 channel 的规矩¶
三条,违反就是事故:
- 只能由发送方关闭,且确定不再发了才关;
- 往已关闭的 channel 发送 → 运行时 panic("send on closed channel");
- 从已关闭的 channel 接收 → 立即返回零值和 ok=false,所以
for v := range ch循环能在 channel 关闭时自动退出——「收完即走」靠的就是这条14。
6. 主线例子(一):发送者—接收者邮箱,完整走一遍¶
sendReceive.go 是本章的机制主线:一个 sender 往 channel 放 1–5,一个 receiver 取, 取完各自报平安。我们按时间线走,旁边是程序真实输出的交错顺序15:
t=0 sender:Sending 1 → 无缓冲,handshake 等 receiver
t≈0 receiver:received 1 → sender 放行,睡 300ms;receiver 睡 500ms(更慢)
t≈300 sender:Sending 2 → 又一次 handshake
t≈500 receiver:received 1(处理完) → 收下 2
……(节奏由两边的 sleep 决定,channel 自动对表)
t=1500 sender:Sending 5 → received 5
t=1800 sender:closing channel(close(ch))
t=2000 receiver:range 循环感知关闭 → 退出循环 → 往 done channel 发 true
main <-done 收到 → 打印 Main: all done!
关键机制每一步都在场:无缓冲 channel 的发送阻塞(对表点)、
for num := range ch 的收完即走、第二个 channel done 作为完成信号。
main 用 <-done 等 receiver——注意这又是一个「主函数等 worker」的姿势,
和第 4 节的 WaitGroup 互为替代。
7. select:同时监听多条线,配超时¶
select 像 switch,但分支是 channel 操作:所有分支同时被考察,哪个先就绪走哪个;
多个同时就绪,运行时随机挑一个;全都没就绪就阻塞;select{} 永远阻塞16。
书上 select.go 的例子:两个 worker 各睡 0–1000ms 随机时长,谁先醒谁的结果先被收:
select {
case msg1 := <-ch1: fmt.Println("Received:", msg1)
case msg2 := <-ch2: fmt.Println("Received:", msg2)
}
某次运行:Received: Worker 2 finished after 53ms 先于 Worker 1 finished after 270ms——
顺序由随机睡眠决定,不由代码书写顺序决定17。
代码里还有个类型细节:ch chan<- string(参数里箭头的位置)表示「只能发的 channel」,
把「只能读」写错成「往里写」会在编译期报错——channel 方向的类型检查18。
超时:给无限等待装个闸门¶
生产环境的真理(原话):「无限期的等待和故障没有区别。」19
给 select 加一个 case <-time.After(2 * time.Second): 分支,2 秒内没人来就走超时路。
演示:shortTask 睡 1 秒、longTask 睡 5 秒,都设 2 秒超时——
输出 Short: Short task finished 和 Long: Timeout! Took too long.20。
8. 主线例子(二):亲手制造一次数据竞争,再用工具抓获¶
raceF1.go:全局变量 counter,5 条 goroutine 各 counter++ 1000 次,
WaitGroup 保证都干完再打印。期望值 5 × 1000 = 5000。实际呢?21
少了 1718 次。 为什么?counter++ 看着是一行,实际是三步:读出、加一、写回。
两条 goroutine 同时做这件事,交叠是这样的(时间从上往下):
goroutine A goroutine B counter
读出 1000 1000
读出 1000 1000
加一得 1001
加一得 1001
写回 1001 1001 ← B 的那一次加一丢了!
写回 1001
图说:两次加一只生效了一次——竞争(race)不是「结果随机」,是「更新无声地丢失」。 而且 WaitGroup 只管「等齐」,管不了「别抢」——同步和互斥是两件事22。
抓获:go run -race¶
$ go run -race ch03/raceF1.go
WARNING: DATA RACE
Read at 0x000104f2b0d0 by goroutine 8:
main.main.func1() raceF1.go:21 +0x88
Previous write at 0x000104f2b0d0 by goroutine 9: ...
Final counter value (expected 5000): 4000
Found 2 data race(s)
exit status 66
race detector 的原理(书里有):给二进制插桩,监视运行期每一次内存访问, 发现「多 goroutine 同时访问同一变量、且至少一个是写」就报, 附冲突双方的调用栈23。代价是变慢、费内存——所以它是开发和测试工具,不是生产配置。
修复:mutex,以及一个 defer 的权衡¶
mutex(互斥锁)保证同一时刻只有持锁者能进临界区——改完预期 5000 稳稳拿到24。 配套知识三条:
sync.RWMutex:读远多于写时用——读者可以多人同时持锁,写者独占。 写频繁时它比 Mutex 没有优势25。defer mu.Unlock()是默认纪律,防早退路径漏解锁;但锁住的函数别太长—— defer 到函数返回才解锁,长函数等于长时间占锁。短函数 defer,长函数尽早手动 Unlock26。- 忘掉 Unlock = 另一类死锁:程序不崩,就是不动(挂起)。
9. 主线例子(三):亲手制造一次死锁,再看运行时怎么报¶
deadlock.go:无缓冲 channel,worker 想发 42,main 也想发 99,没有任何人接收:
worker goroutine: ch <- 42 ← 阻塞,等接收
main goroutine: ch <- 99 ← 也阻塞,等接收
→ fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]: main.main() deadlock.go:22
goroutine 35 [chan send]: main.main.func1() deadlock.go:15
图说:两条 goroutine 互相等待一个永远不会来的条件,围成环。
Go 运行时会自动侦测(全部 goroutine 都睡着、无人能醒来时 panic 并报出每条卡在哪),
这是第 ① 个例子的黑暗镜像——那次是 main 不等人跑了,这次是都等别人谁也不动。
修复简单到一句话:一发必须配一收;把 main 改成 val := <-ch,输出立刻正常27。
10. 设计根治:矩阵乘法的「各写各格」¶
第 8 节用锁挡住竞争,第 10 节展示更高的境界:结构设计让竞争不可能发生。
矩阵乘法 result[i][j] = A 的第 i 行 · B 的第 j 列(点积,m 次乘加)。
书上的并发方案:给结果矩阵的每个格子开一条 goroutine——
for i := 0; i < n; i++ {
for j := 0; j < k; j++ {
wg.Go(func() { // 每条 goroutine 算一个格子
sum := 0
for l := 0; l < m; l++ { sum += A[i][l] * B[l][j] }
result[i][j] = sum // Safe: each goroutine writes to a unique cell
})
}
}
没有锁、没有 channel,却没有竞争——因为每条 goroutine 只写自己的格子,
写集两两不相交28。书上就着这个例子说了一句全章的题眼:
「先仔细想程序结构,再考虑要不要 mutex、channel、select。」
顺带一个真实输出(2×3 乘 3×2):[0 1 -1] × [[0 0],[0 1],[-1 0]] = [1 1]——
手算一格验证:第 1 行第 1 列 = 0×0 + 1×0 + (−1)×(−1) = 1,吻合29。
11. 两个并发模式:worker pool 与 monitor¶
worker pool(workerPool.go):固定 N 条 worker 从共享 jobs channel 抢活, 结果写 results channel;干完的条件链是「jobs 发完即 close → worker 的 range 退出 → WaitGroup 归零 → close(results)→ main 的 range 退出」。 输出里 job 的完成顺序天然乱序(2 个 worker 处理 5 个 job)30。 书里的选择判据值得带走:资源受限(内存大、API 限流)用池子限并发;轻量任务直接开 goroutine, 池子是多余的复杂31。
monitor goroutine(monitor.go):一条 goroutine 独占一个计数器, 所有人通过 channel 发请求(请求里内嵌一条 reply channel 拿回执):
type Request struct { Increment int; Reply chan int }
// monitor:count += req.Increment;req.Reply <- count
3 个 worker 各加 2 次,输出里「谁看到哪个数」乱序(Worker 0 看到 6 先于 Worker 1 看到 5), 但计数器本身 1,2,3,4,5,6 一个不少——状态只有一个人在改,竞争从结构上不存在32。 这就是那句口号的最完整演示:不共享内存,只通信,而共享自然发生。
12. 作者的判断与证据¶
| 判断 | 性质 | 依据 |
|---|---|---|
| 通信优先于共享内存 | Go 官方哲学,作者奉为纲 | 口号原文 + monitor 例子是全书最贴的演示232 |
| goroutine 要用幂等性把关 | 作者自己的经验法则 | 「A critical rule of thumb」原段8 |
| channel 只能由发送方关闭 | 语言规则 + 社区铁律 | send on closed channel 会 panic 的机制说明14 |
| 结构设计优先于锁 | 作者明确的工程主张 | 矩阵乘法一节的原话题眼28 |
| -race 是开发期必备、不上生产 | 工具事实 + 作者建议 | 插桩原理与开销说明23 |
| worker pool vs 直接开 goroutine 按资源约束选 | 作者的取舍判据 | 章内 Note31 |
13. 边界与局限¶
- sync/atomic 完全没讲。 计数器这个例子最贴的修法其实是原子操作 (atomic.AddInt64),比 mutex 快一个量级;书中只给了 mutex 一条路。
- context 取消没进本章。 超时只教了
time.After;「上游喊停、下游级联退出」这套 生产环境的核心机制,要等第 9 章 context 一节。 - channel 内存模型(formal memory model)没讲。 「happens-before」这类保证 一句未提,读者得自己相信「channel 传数据顺带同步」。
- race detector 的覆盖盲区没讲:它只抓「同一进程内」的交叠;跨进程的共享 (数据库、文件)它看不见。
- goroutine 泄漏只是超时一节暗示了一下(leaking resources), 没有作为独立题目教——第 13 章的「goroutine leak profile」(Go 1.26)才回来补。
14. 可带走的¶
go关键字开 goroutine;main 一退全部蒸发——等齐用 WaitGroup,一对一报善用 done channel。- WaitGroup 三件套 Add/Done/Wait;Go 1.25 的
wg.Go()把三步合一,优先用它。 - 循环变量按引用捕获;Go 1.22 起 for range 每轮自动换新变量,旧代码里的
i := i不再必要。 - 无缓冲 channel = 当面交接(强同步);缓冲 channel = 信箱(capacity 是领先额度)。
- 关闭规矩:发送方关、只关一次、往关闭的 channel 发会 panic、range 靠「收完即走」退出。
- select 同时监听多个 channel;加
time.After分支 = 超时;无限等待 = 故障。 - race 的机理是「读-加-写」三步交叠丢更新;
go run -race抓现行;短临界区用 defer Unlock,长临界区别。 - 死锁 = 全员睡着围成环;运行时会自动报;一发配一收是不二法门。
- 矩阵乘法式的设计根治:让写集不相交,比锁更可靠——先想结构,再上锁。
- 共享状态交给一条 monitor goroutine,请求里内嵌 reply channel——Go 哲学的完整形态。
15. 原文地图¶
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 并发是思维模型、章节立场 | Learning about goroutines | text/34-ch03.txt:6(搜「not as an optimization」) |
| 「通过通信共享内存」口号 | Learning about goroutines | text/35-fm-learning-about-goroutines.txt:32(搜「do not communicate」) |
| GOMAXPROCS 与线程池 | Learning about goroutines | text/35-fm-learning-about-goroutines.txt:23(搜「GOMAXPROCS」) |
| main 不等人的第一个例子 | Learning about goroutines | text/35-fm-learning-about-goroutines.txt:109(搜「Starting 20 goroutines」) · :124(搜「remaining 17 goroutines」) |
| 循环变量按引用捕获、Go 1.22 修复 | Learning about goroutines | text/35-fm-learning-about-goroutines.txt:84(搜「by reference」) · :90(搜「variable shadowing」) |
| 幂等性判据 | Learning about goroutines | text/35-fm-learning-about-goroutines.txt:224(搜「idempotency」) |
| WaitGroup 用法与失衡后果 | Learning about goroutines | text/35-fm-learning-about-goroutines.txt:150(搜「sync.WaitGroup」) · :192(搜「crucial that every call」) |
| WaitGroup.Go(Go 1.25) | Learning about goroutines | text/35-fm-learning-about-goroutines.txt:245(搜「new in Go 1.25」) |
| channel 关闭规矩 | Learning about channels | text/36-fm-learning-about-channels.txt:16(搜「once closed」) |
| select 语义 | Learning about channels | text/36-fm-learning-about-channels.txt:30(搜「select keyword」) · :44(搜「examined simultaneously」) |
| 邮箱主线例子(sendReceive) | Learning about channels | text/36-fm-learning-about-channels.txt:153(搜「func sender」) · :212(搜「Sender: sending 1」) |
| 缓冲 vs 无缓冲输出对比 | Learning about channels | text/36-fm-learning-about-channels.txt:338(搜「bufUnbuf.go」) · :371(搜「fundamental difference」) |
| 超时与「无限等待=故障」 | Learning about channels | text/36-fm-learning-about-channels.txt:402(搜「indistinguishable from a failure」) · :477(搜「Timeout! Took too long」) |
| worker pool 与选择判据 | Learning about channels | text/36-fm-learning-about-channels.txt:494(搜「worker pool」) · :668(搜「resource constraints」) · :640(搜「2 workers for 5 jobs」) |
| monitor goroutine | Learning about channels | text/36-fm-learning-about-channels.txt:749(搜「Monitor goroutines」) · :776(搜「type Request struct」) · :835(搜「Worker 0 sees counter」) |
| 竞争制造与 -race 抓获 | Catching deadlocks and race conditions | text/37-fm-catching-deadlocks-and-race-conditions.txt:65(搜「counter++」) · :85(搜「expected 5000」) · :93(搜「WARNING: DATA RACE」) |
| mutex 修复、RWMutex、defer 解锁权衡 | Catching deadlocks and race conditions | text/37-fm-catching-deadlocks-and-race-conditions.txt:179(搜「mu.Lock」) · :205(搜「RWMutex」) · :221(搜「defer mu.Unlock」) |
| 死锁制造与运行时侦测 | Catching deadlocks and race conditions | text/37-fm-catching-deadlocks-and-race-conditions.txt:434(搜「blocks forever」) · :467(搜「all goroutines are asleep」) |
| 矩阵乘法各写各格 | Catching deadlocks and race conditions | text/37-fm-catching-deadlocks-and-race-conditions.txt:367(搜「unique cell」) · :390(搜「go run ch03/matrix.go」) |
-
出处:「Go Concurrency」第 5–8 段(
text/34-ch03.txt:6,搜「not as an optimization」)。「we must embrace concurrency not as an optimization but as a model of thought」。 ↩ -
出处:「Learning about goroutines」第 29–38 段(
text/35-fm-learning-about-goroutines.txt:32,搜「do not communicate by sharing memory」)。这是 Go 社区的官方口号,CSP 模型(通信顺序进程)的根子书里未展开,属通用知识。 ↩↩ -
出处:「Learning about goroutines」第 40–45 段(
text/35-fm-learning-about-goroutines.txt:41,搜「go keyword」)。「When you prefix a function or method call with the go keyword, Go schedules that call to run asynchronously」。 ↩ -
出处:「Learning about goroutines」第 20–28 段(
text/35-fm-learning-about-goroutines.txt:23,搜「GOMAXPROCS」)。原文:运行时可以多开线程处理阻塞的系统调用,但同时跑 Go 代码的线程数不超过 GOMAXPROCS。 ↩ -
出处:「Learning about goroutines」第 11–15 段(
text/35-fm-learning-about-goroutines.txt:13,搜「os.Exit」)。 ↩ -
出处:「Learning about goroutines」(
text/35-fm-learning-about-goroutines.txt:109,搜「Starting 20 goroutines」;:111,搜「exiting without waiting」;:124,搜「remaining 17 goroutines」)。 ↩ -
出处:「Learning about goroutines」Note(
text/35-fm-learning-about-goroutines.txt:84,搜「by reference」;:90,搜「variable shadowing」;:91,搜「Go 1.22」)。 ↩ -
出处:「Learning about goroutines」Note(
text/35-fm-learning-about-goroutines.txt:220,搜「overuse」;:224,搜「idempotency」)。「A critical rule of thumb… is the concept of idempotency」。 ↩↩ -
出处:「Learning about goroutines」(
text/35-fm-learning-about-goroutines.txt:150,搜「sync.WaitGroup」;:204,搜「Processing: task2」;:216,搜「always printed last」)。 ↩ -
出处:「Learning about goroutines」第 192–200 段(
text/35-fm-learning-about-goroutines.txt:192,搜「crucial」)。Add 5 次 Done 4 次的失衡示例与 negative counter panic 都在同段。 ↩ -
出处:「Learning about goroutines」第 245–253 段(
text/35-fm-learning-about-goroutines.txt:245,搜「new in Go 1.25」)。「combining the counter increment, goroutine launch, and deferred Done() call into a single step」。 ↩ -
出处:「Learning about goroutines」第 29–38 段(
text/35-fm-learning-about-goroutines.txt:29,搜「cannot communicate with each other directly」)。「there is no shared memory in the traditional way… unless we are dealing with global variables」。 ↩ -
出处:「Learning about channels」(
text/36-fm-learning-about-channels.txt:338,搜「bufUnbuf.go」;:377,搜「strict alternating」;:384,搜「racing ahead」)。两段输出分别在第 338–369 行。 ↩ -
出处:「Learning about channels」第 16–23 段(
text/36-fm-learning-about-channels.txt:16,搜「once closed」)。「if you write to a closed channel, Go panics at runtime with the "send on closed channel" error」;接收得零值 + ok=false 在同段。 ↩↩ -
出处:「Learning about channels」(
text/36-fm-learning-about-channels.txt:153,搜「func sender」;:212,搜「Sender: sending 1」;:223,搜「Sender: closing channel」)。完整交错输出见第 212–225 行。 ↩ -
出处:「Learning about channels」第 29–49 段(
text/36-fm-learning-about-channels.txt:36,搜「select{}」;:44,搜「examined simultaneously」;:48,搜「random selection」)。 ↩ -
出处:「Learning about channels」(
text/36-fm-learning-about-channels.txt:124,搜「select.go」;:125,搜「Worker 2 finished after 53ms」)。 ↩ -
出处:「Learning about channels」第 106–109 段(
text/36-fm-learning-about-channels.txt:107,搜「chan<- string」)。「it can only send data, not receive it – this is a safety feature」。 ↩ -
出处:「Learning about channels」第 401–403 段(
text/36-fm-learning-about-channels.txt:402,搜「indistinguishable from a failure」)。 ↩ -
出处:「Learning about channels」(
text/36-fm-learning-about-channels.txt:448,搜「case result := <-shortDone」;:477,搜「Timeout! Took too long」)。 ↩ -
出处:「Catching deadlocks and race conditions」(
text/37-fm-catching-deadlocks-and-race-conditions.txt:65,搜「counter++」;:85,搜「expected 5000」)。3282 是该次运行值,原文注明「typically prints a value less than 5,000」。 ↩ -
出处:「Catching deadlocks and race conditions」第 73–90 段(
text/37-fm-catching-deadlocks-and-race-conditions.txt:80,搜「does not prevent the race itself」)。「sync.WaitGroup ensures that the main() function waits… but it does not prevent the race itself」。 ↩ -
出处:「Catching deadlocks and race conditions」第 20–36 段(
text/37-fm-catching-deadlocks-and-race-conditions.txt:23,搜「-race flag」)。「instruments the binary and monitors all memory accesses during execution」;开销与退出码 66 见第 27–30、141 行。 ↩↩ -
出处:「Catching deadlocks and race conditions」(
text/37-fm-catching-deadlocks-and-race-conditions.txt:179,搜「mu.Lock」)。修复版代码与「reliably matches the expected 5,000」见第 187–204 段。 ↩ -
出处:「Catching deadlocks and race conditions」第 205–213 段(
text/37-fm-catching-deadlocks-and-race-conditions.txt:205,搜「RWMutex」)。「allows multiple readers to hold the lock simultaneously」。 ↩ -
出处:「Catching deadlocks and race conditions」第 214–233 段(
text/37-fm-catching-deadlocks-and-race-conditions.txt:221,搜「defer mu.Unlock」;:224,搜「relatively short」)。长短函数的权衡在同段。 ↩ -
出处:「Catching deadlocks and race conditions」(
text/37-fm-catching-deadlocks-and-race-conditions.txt:434,搜「blocks forever」;:467,搜「all goroutines are asleep」;:504,搜「val := <-ch」;:529,搜「Main: received 42」)。 ↩ -
出处:「Catching deadlocks and race conditions」(
text/37-fm-catching-deadlocks-and-race-conditions.txt:367,搜「unique cell」;:260,搜「think carefully」)。「Safe: each goroutine writes to a unique cell」是代码注释原文。 ↩↩ -
出处:「Catching deadlocks and race conditions」(
text/37-fm-catching-deadlocks-and-race-conditions.txt:390,搜「go run ch03/matrix.go」)。输出与矩阵维度见第 390–400 行。 ↩ -
出处:「Learning about channels」(
text/36-fm-learning-about-channels.txt:641,搜「2 workers for 5 jobs」;:594,搜「wg.Go」)。jobs/results 双 channel 与关闭链见第 589–618 行。 ↩ -
出处:「Learning about channels」Note(
text/36-fm-learning-about-channels.txt:671,搜「resource constraints」)。「opt for a worker pool when dealing with heavy resource constraints」。 ↩↩ -
出处:「Learning about channels」(
text/36-fm-learning-about-channels.txt:749,搜「Monitor goroutines」;:776,搜「type Request struct」;:835,搜「Worker 0 sees counter」;:848,搜「1, then 2」)。 ↩↩