编程语言研究书库

Go 并发 — goroutine、channel 与同步的一整张地图

Go 并发 — goroutine、channel 与同步的一整张地图

这一章讲什么: Go 并发机制的全部家底——goroutine 和 channel 怎么工作、 有缓冲和无缓冲差在哪、七种协作模式、共享内存怎么用锁保住、竞态怎么抓、 性能怎么调。 它在全书链条里的位置: 第 01 章把并发列为 Go 的三大招牌之一,这一章把招牌 拆开给你看构造。第 08 章(并发流水线)是本章模式的工程化,第 10 章的性能调优 是本章 profiling 的生产版。 主线例子: 从「1000 个 goroutine 抢一个计数器」这个会算错的程序出发, 逐节修好它,最后长成一条流水线。每一步旁边都有具体的数和状态。

1. 顶层全景:CSP 模型的一张图

Go 的并发模型师承 CSP(Communicating Sequential Processes,通信顺序进程—— 一个并发计算理论,核心主张:并发单元之间靠传消息协作,而不是抢共享内存)1。

  goroutine ──► channel ──► goroutine ──► channel ──► goroutine
  (轻量执行单位)  (传送带)      (worker)     (传送带)     (收集结果)
      │                        │                         │
      └──── sync 包:万不得已要共享内存时,用锁保住临界区 ────┘
      └──── select:同时等好几条 channel,谁先来处理谁

四样原语(最基础、不可再拆的操作单元)的分工:go 起执行、channel 传数据、sync 保共享、select 多路等待; context 管取消(第 06 章专讲)2。

主线例子的起点(原书的例子): 期望输出 1000,实际输出每次都不一样:

var counter = 0
func main() {
    for i := 0; i < 1000; i++ {
        go func() {
            counter++ // unsafe
        }()
    }
    time.Sleep(time.Second)
    fmt.Println(counter)
}

(在慢机器上它可能恰好打出 1000——「跑对了」不代表没竞态,这是并发 bug 最阴险之处。3)

2. goroutine:轻在哪,谁在调度

解决什么问题: 操作系统线程(OS thread)开一份就要兆字节(MB)级的栈; 一台服务器同时处理几万连接,线程模型要么开不起,要么开得起但调度开销吃掉一半 CPU。

goroutine 的做法: 启动时栈只有几千字节(KB 级),不够用再动态长、 不用了再缩回——所以一个进程里开成千上万乃至百万个goroutine 是常态4。 调度由 Go 运行时自己干,模型叫 G-M-P5:

字母是什么
Ggoroutine 本身(一件待干的活)
M操作系统线程(真正干活的劳力)
P逻辑处理器,管着一条待运行队列

多个 G 复用少数 M;某个 P 的队列空了,去别的 P 那里偷活干(work stealing); 跑得太久的 G 会被异步抢占——不会一个死循环拖垮全家5。 goroutine 也可能主动挂起(等 channel、等锁、等 I/O),挂起时调度器把 CPU 让给别人5。

创建: go doWork() 一行;也可以起匿名函数的:go func(){ ... }()6。

主线例子第 ① 个 bug:循环变量捕获

for i := 0; i < 5; i++ {
    go func() {
        fmt.Println(i)   // 五个 goroutine 可能全打 5!
    }()
}

为什么错: 闭包捕获的是变量 i 本身,不是当时的值。循环结束时 i 已是 5, goroutine 真正运行时读到的就是那个 57。 修法(原书的惯用法): 把值当参数传进去——参数是拷贝:

for i := 0; i < 5; i++ {
    go func(v int) {
        fmt.Println(v)
    }(i)
}

(注:新版本 Go 已修改循环变量语义、每轮一个新变量,这个坑正在消失;但读懂它 才能看懂旧代码和 AI 给出的老写法。7)

3. channel:有缓冲与无缓冲,差的是「背压」

3.1 无缓冲:每一次传递都是一次握手

ch := make(chan string)——缓冲为零。发送和接收必须同时到场: 发送方把值递出去的那一瞬间就被停住,直到有人接;接收方没东西收时也停住,直到有人发。 原书比喻:接力交棒,两人必须在同一时刻碰到棒8。

ch := make(chan string)
go func() { ch <- "task done" }()   // 停在这里,直到有人接
msg := <-ch                          // 停在这里,直到有人发
fmt.Println(msg)

用途:严格同步、完成信号、「确保那件事干完了才继续」8。

3.2 有缓冲:一条有容量的队列

ch := make(chan int, 5)——能暂存 5 个值。缓冲区没满,发送不阻塞; 满了才变回同步行为9。生产快、消费慢时,缓冲吸收波动; 但缓冲越大,问题被掩盖得越久——消费者卡死,生产者还能若无其事地塞, 直到塞满那一刻大家一起 deadlock9。

3.3 背压(backpressure):系统的「别塞了」

背压指消费跟不上时,系统天然地拖慢生产。无缓冲 channel 提供即时背压 (每发必等);有缓冲的把背压推迟到缓冲塞满10。 选哪个,看 correctness 和 throughput 谁更要紧:要保护内存和下游系统 → 无缓冲或 小缓冲;要吞吐 → 大缓冲10。原书还拆了四个常见误解,值得各记一句11:

误解实情
有缓冲总更快大缓冲会掩盖失衡和死循环,内存用量不可预测
无缓冲太慢需要同步时它反而省掉缓冲管理的开销
有缓冲就不用锁了只在数据「全走 channel」时成立;共享内存照旧要锁
死锁只发生在无缓冲缓冲满了、没人收,一样死锁

4. 七个协作模式(主线例子在这里长成流水线)

模式一:生产者–消费者。 生产者造任务投进 channel,消费者 range 取出处理; 生产者 close(channel),消费者的 range 自动结束12:

tasks := make(chan int, 5)
go func() { for i := 1; i <= 10; i++ { tasks <- i }; close(tasks) }()
go func() { for t := range tasks { fmt.Println("Processing", t) } }()

模式二:流水线(pipeline)。 每个环节一个 goroutine:收上游 channel、 变换、发下游 channel、close 下游通知收工。第 08 章把它工程化,这里记住形状: stage1 → stage2 → stage3,环节之间只靠 channel 说话13。

模式三:扇出/扇入(fan-out / fan-in)。 扇出:起 N 个 worker 同抢一个 jobs channel; 扇入:把 N 个 worker 的结果收拢到一个 results channel。主线例子的计数器修好后就长这样 (见 §6)14。

模式四:令牌通道(token channel)——用 channel 当信号量。 一个容量为 3 的缓冲 channel 里放 3 个令牌;goroutine 干活前取一个、干完还一个——同一时刻只有 3 个 在跑,不用一行锁15:

sem := make(chan struct{}, 3)
for i := 0; i < 10; i++ {
    go func(id int) {
        sem <- struct{}{}        // 取令牌;拿不到就等
        fmt.Println("Running job", id)
        time.Sleep(time.Second)
        <-sem                    // 还令牌
    }(i)
}

模式五:信号通知。 done := make(chan struct{})——不传数据,传「发生了」这个事实: 发一个空结构体、接收方等它。优雅退出(模式七)和跨 goroutine 的「等我干完」都用它16。

模式六:消灭共享——地图管理员 goroutine。 与其给 map 加锁,不如让一个 goroutine 独占这个 map,其他人想读写就发消息问它(查询里附带一个用于收答案的 reply channel)。 数据所有权在 goroutine 之间手递手转移,竞态从结构上消失。这就是常说的 「不要通过共享内存来通信,要通过通信来共享内存」17。

模式七:优雅退出。 专门的 quit channel;每个 goroutine 的主循环用 select 一边干活一边听 quit,close(quit) 全员有序收工18:

select {
case <-quit:
    return
default:
    // 正常干活
}

4.8 两个进阶原语走一遍:sync.Cond 与 sync.Once

表格太抽象,这两个值得各看一个真例子。

sync.Cond:有界队列的「等货/进货」信号。 场景:一个 goroutine 往队列放数据, 一个往外取;取的时候队列可能空,空了就睡,不占 CPU,等放的人叫醒—— 这就是条件变量(含义:一个「条件可能成立了」的广播站,睡着的 goroutine 挂上去等,条件成立的人负责广播):

var (
    mu   sync.Mutex
    cond = sync.NewCond(&mu)   // 条件变量必须配一把锁
    data = make([]int, 0)
)

func producer() {
    mu.Lock()
    data = append(data, 42)
    cond.Signal()              // 叫醒一个正在等的消费者
    mu.Unlock()
}

func consumer() {
    mu.Lock()
    for len(data) == 0 {       // 必须用 for 不是 if:醒来要再查一遍条件
        cond.Wait()            // 睡着;Wait 内部会临时放锁
    }
    v := data[0]
    data = data[1:]
    mu.Unlock()
    fmt.Println("Consumed:", v)
}

原书点名的坑:漏信号(Signal 发在 Wait 之前,消费者错过)、死锁(忘了在 锁内 Wait)、假唤醒(所以醒来必须再查条件,for 不能省)。这三个坑每一个 都是生产事故的形状,Cond 也因此成了 sync 包里最需要敬畏的一个。

sync.Once:懒初始化的保险。 有些初始化很贵(建连接、加载大表),想等第一次 真用到再做、且只做一次——sync.Once 的 Do(func(){...}) 保证即使在并发下 也恰好执行一次。它和「自己用布尔变量+锁实现」的区别只有一条:Once 把这个 易错的模式收进标准库,你不用每次重新踩一遍其中的竞态。

4.9 什么时候不用 channel 用锁(选择口径)

原书通篇「优先 channel」,但 §6 的表格给了反方向的线索。一条实用的口径:

  • 数据所有权在流动(生产者→消费者,谁拿着谁负责) → channel;
  • 数据定居在一处(一个配置表、一个缓存、一个计数器,大家都要来看) → 锁;
  • 读远多于写 → RWMutex;只是几个整数计数 → 原子操作(第 08 章展开)。

选错的代价不对称:该用锁的地方硬套 channel,会造出一个「地图管理员 goroutine」, 每次读数据都要发消息等回复,延迟凭空多一趟通信;该用 channel 的地方用锁, 则是把所有权弄糊,竞态风险回来。默认 channel,是真有定居数据时才用锁—— 这个次序本身也是原书的口径。

5. select:同时等好几路

select 和 switch 形似神不同:它在多个 channel 操作同时阻塞、谁先到执行谁; 配 time.After 就是超时,配 ctx.Done() 就是可取消——HTTP 客户端、流水线、 worker pool 的取消全指着它。(原书在退出模式和上下文两处处使用;第 06 章 context 是它的最大用家。18)

6. 竞态与 sync 包:主线例子修好它

竞态的严格定义,三条同时成立19:

  1. 两个以上 goroutine 并发访问同一变量;
  2. 至少一个在写;
  3. 没有同步定序。

counter++ 不是原子操作,是「读-改-写」三步;两个 goroutine 同时读到 100、 各自加一、先后写回 101——丢了一次加。期望 1000,打印 982 就是这么来的19。 -race 能抓(第 04 章),但要消除,得上同步。原书给的全家福20:

原语干什么主线例子的角色
sync.Mutex互斥锁,同一时刻只放一个进临界区修好计数器
sync.RWMutex读锁共享、写锁独占配置中心那种读多写少
sync.WaitGroup等一组 goroutine 全部完工替代 sleep 的瞎等
sync.Once某初始化只执行一次懒汉单例
sync.Cond等「条件成立」的信号有界队列
sync.Map并发安全的 map读多写少的共享表
sync.Pool对象复用,减 GC 压力高频临时对象

主线例子第 ② 步:Mutex 修计数器

var (
    counter = 0
    mu      sync.Mutex
)
func increment() {
    mu.Lock()
    counter++
    mu.Unlock()
}

锁住了「读-改-写」三步,三步变原子21。两条配套纪律:

  • defer mu.Unlock() 紧跟 Lock——早返回、panic 路径都不会忘解锁(第 04 章 defer 的兑现)22;
  • 粒度权衡:一把大锁简单但并行度低;每个字段一把锁并行高但易死锁。 没有标准答案,是设计题23。

主线例子第 ③ 步:WaitGroup 替代 sleep

time.Sleep(time.Second) 是「大概等够了」的瞎猜;WaitGroup 是点名报数:

var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
    wg.Add(1)
    go func() {
        defer wg.Done()
        increment()
    }()
}
wg.Wait()              // 1000 个全报到了才往下走
fmt.Println(counter)   // 这次确定是 1000

Add(1) 记名、Done() 销号、Wait() 等清零24。程序不等完就退出的 「半成品写盘」事故,用这个根除。

RWMutex:读多写少的放大器

RLock() 读锁可以多个同时持有;Lock() 写锁独占。配置、缓存这类 「千次读一次写」的结构,吞吐显著提升25。边界:读者连绵不断时写者会饿死—— Go 的实现尽量缓解,但设计上要意识到25。

happens-before:锁为什么够用

光加锁不够,还要理解内存可见性。Go 规定了几条「happens-before」26:

动作保证
Mutex 解锁happens-before 之后的加锁(这次的写,下次的读看得见)
channel 发送happens-before 对应的接收
close(channel)happens-before 收到零值

没有 happens-before 关系的写,读到什么是未定义的。锁和 channel 不只是 「别同时进」,还是「内存改动按序可见」的合同。

6.2 竞态的微观时间线:982 是怎么丢出来的

「丢更新」三个字太抽象,把第 1 节那个计数器在微观层面展开(数字为演示): 两个 goroutine 同时执行 counter++,这个操作其实是三步——

  时刻1:goroutine A 读 counter = 980
  时刻2:goroutine B 读 counter = 980   (A 还没写回)
  时刻3:A 加一,写回 981
  时刻4:B 加一,写回 981                  (覆盖了 A 的那次!)

两次加一,结果只涨了一。每一次「恰好同时」都是调度器随机赏的, 所以程序可能跑对一千次、在第一千零一次出 982——这也解释了为什么 「我本地没复现」不是证据,-race 的报告才是:它不赌时序,直接监视 每次内存访问有没有按 happens-before 排序。

6.3 锁粒度的实战口径(比原则更具体)

「粗锁简单、细锁快」之外,三条可以直接用的口径:

  1. 先粗后细:第一版一把大锁,profile 显示竞争成热点了再拆——过早细分 是优化之坑;
  2. 临界区里别调别人:锁内调用你不控制的代码(回调、网络),等于把锁的 寿命交给外人,死锁和性能惨案都从这来;
  3. 读路径先想 RWMutex,再想原子,最后才锁:三个方案按「实现成本」从低到高 是反的,按「性能」从低到高也是反的——取中。

7. 性能画像:工具链 + AI 的六步循环

7.1 先能看:开 pprof

import _ "net/http/pprof"   // 下划线导入:只为 init 注册处理器

go func() {
    http.ListenAndServe(":6060", nil)   // 浏览器/pprof 命令连这个端口
}()

pprof 能采样(周期性掐点统计,用样本推断全貌)CPU 时间、内存分配、goroutine 调度、阻塞事件、锁竞争27。 go tool pprof cpu.out 看火焰图;goroutine dump 看每个 goroutine 卡在哪行。

7.2 pprof 常见的六种病征28

goroutine 无上限疯涨(该用 worker pool)、锁粒度太粗(竞争热点)、channel 阻塞 (背压或饿死)、调度失衡、超时/取消优先级不对引发重试风暴、未同步访问。

7.3 worker pool:本章最重要的优化模式

解决什么问题: for _, task := range tasks { go handle(task) }——任务一多, goroutine 数量爆炸,上下文切换和 GC 压力一起吃光 CPU。 做法: 固定 N 个工人循环取任务;任务队列本身(有界 channel)就是天然的背压—— 生产者塞满就慢下来29:

jobs := make(chan Task, 100)
// N 个工人
for w := 0; w < 8; w++ {
    go func() {
        for j := range jobs { handle(j) }
    }()
}

7.4 AI 辅助的六步流程(原书的方法论,值得照抄)

  1. 采数据:go test -cpuprofile 或连线上 :6060;火焰图、goroutine dump、 trace、锁竞争文件各取所需30;
  2. 喂 AI:太大就剪最热的 20–50 行——「减过肥的输入 AI 消化得最好」30;
  3. 问具体问题(原书原话级别的示例):「解释这些 goroutine 为什么阻塞」 「这份 mutex trace 怎么减少竞争」「这段代码会有竞态吗,怎么修」31;
  4. 应用建议:比如把无缓冲 jobs 改成 100 缓冲、把每请求一个 goroutine 换成 有界池32;
  5. 重新画像验证——AI 的优化是假设,数字才是判决;不行就带着新数据再问33;
  6. 迭代到达标。

判断(我们的,不是书里的): 这六步的顺序(测→问→改→再测)比任何单个 技巧都值钱,因为它防住了 AI 辅助并发优化最大的风险:AI 提议的每一项改动 都可能引入新的竞态或死锁,而它自己不会跑 -race。第 4 步和第 5 步之间 永远隔着一次 go test -race ./...。 如果错,会错在: 若负载特征稳定、系统简单,直接照教科书模式(worker pool、 扇入扇出)可能胜过「AI 分析画像」的循环——六步流程的价值随系统复杂度上升, 小项目用它反而是仪式过度。

7.5 优雅关闭的完整时序(把第 06 章的流水线接进来)

第 06 章关的是 HTTP 服务器,这里把流水线的关闭接进同一时间线:

  SIGTERM 到
    ├─ HTTP:停止收新请求,等在途 handler 干完(带 10s 上限)
    ├─ 流水线:close(quit) 或 cancel(),每个环节在 select 里收到信号
    │         带着 defer close(out) 干净退出,一路 close 传回源头
    └─ 数据库:ctx 到期,在途查询中止,事务回滚,db.Close()

三个衔接点容易断:顺序(先停入口,再停加工,最后断仓库——反了会丢数据)、 每个 goroutine 都要有退出路径(第 04 章泄漏清单的对策)、关闭也要限时 (10 秒是上限不是目标,干不完的宁可回滚不要硬等)。原书把这三件事分散在 第 05、06、10 三章,合起来才是完整的关闭语义。

8. 作者的判断与证据

机制层面(可验证): G-M-P 模型、栈动态伸缩、异步抢占、channel 阻塞语义、 happens-before 规则表、race detector、各 sync 原语语义——全是运行时/规范事实582619。 建议层面(经验): 「先 channel 后锁」「defer 解锁」「粗粒度锁起步」是社区 最佳实践,本章未给测量依据;RWMutex 饿死风险的描述与官方文档一致2325。

9. 边界与局限

  • select 语句本身没单独立节——它散落在退出模式和上下文用例里,系统学要去别处。
  • context 包只点了名,取消传播树、value 的用法都推到第 06 章。
  • 原子操作(sync/atomic)只有一句警告(「需要小心使用」),没有示例—— 计数器这类简单场景其实原子操作比 Mutex 更省,读者需自行补充26。
  • AI 一节没有失败案例:喂错数据得出自信结论的风险,只在「Step 2 剪 20–50 行」 里隐性缓解。

10. 可带走的

  1. goroutine 起步 KB 级栈、G-M-P 调度、work stealing、可异步抢占——所以能开几万个。
  2. 循环里起 goroutine 传参 capture;闭包捕变量不捕值。
  3. 无缓冲 channel = 每次交接都握手(即时背压);有缓冲 = 队列(延迟背压)。
  4. 背压是设计选择:保正确性用无缓冲/小缓冲,保吞吐用大缓冲。
  5. 七个模式:生产者-消费者、流水线、扇出扇入、令牌通道、信号通知、地图管理员、优雅退出。
  6. 竞态三条件:同变量、有写、无同步。counter++ 三步走,必丢更新。
  7. Mutex + defer Unlock;粒度是设计题。RWMutex 治读多写少,注意写者饿死。
  8. WaitGroup 点名报数替代 sleep 瞎等。
  9. happens-before:解锁→加锁、发送→接收、close→收零值;没有它,读到什么未定义。
  10. worker pool 治 goroutine 爆炸,任务队列自带背压。
  11. 调优六步:测→喂→问→改→再测→迭代;AI 出假设,-race 和 pprof 出判决。
  12. 两个反直觉:缓冲不是越大越好;无缓冲在需要同步时反而快。

11. 原文地图

主题原书章原文位置
CSP 哲学与四原语5. Concurrency in Gotext/15-ch05-5-concurrency-in-go.txt:64(搜「concurrency paradigm influenced」)text/15-ch05-5-concurrency-in-go.txt:84(搜「The select statement enables」)
G-M-P 与异步抢占同上text/15-ch05-5-concurrency-in-go.txt:132(搜「Understanding Goroutines」)text/15-ch05-5-concurrency-in-go.txt:139(搜「G-M-P」)
循环变量捕获同上text/15-ch05-5-concurrency-in-go.txt:175(搜「Loop Variable Capture」)
无缓冲 channel同上text/15-ch05-5-concurrency-in-go.txt:218(搜「at the exact same moment」)
有缓冲 channel同上text/15-ch05-5-concurrency-in-go.txt:256(搜「behavior becomes synchronous」)
背压同上text/15-ch05-5-concurrency-in-go.txt:332(搜「Backpressure」)
四个误解同上text/15-ch05-5-concurrency-in-go.txt:344(搜「Common Pitfalls and Misconceptions」)
生产者-消费者同上text/15-ch05-5-concurrency-in-go.txt:382(搜「Producer–Consumer」)
流水线同上text/15-ch05-5-concurrency-in-go.txt:413(搜「Pipeline Communication」)
扇出/扇入同上text/15-ch05-5-concurrency-in-go.txt:455(搜「Fan-out and Fan-in」)
令牌通道同上text/15-ch05-5-concurrency-in-go.txt:496(搜「Token Channels」)
地图管理员同上text/15-ch05-5-concurrency-in-go.txt:542(搜「Avoiding Shared State」)
优雅退出同上text/15-ch05-5-concurrency-in-go.txt:572(搜「Graceful Shutdown」)
竞态三条件与计数器同上text/15-ch05-5-concurrency-in-go.txt:623(搜「Race Conditions in Go」)
sync 全家与 Mutex同上text/15-ch05-5-concurrency-in-go.txt:648(搜「Overview of the Sync」)text/15-ch05-5-concurrency-in-go.txt:664(搜「sync.Mutex」)
WaitGroup同上text/15-ch05-5-concurrency-in-go.txt:726(搜「not directly used for preventing data races」)
RWMutex 与饿死同上text/15-ch05-5-concurrency-in-go.txt:700(搜「Reader/Writer Locking」)
happens-before 表同上text/15-ch05-5-concurrency-in-go.txt:787(搜「Happens-before」)
pprof 开启同上text/15-ch05-5-concurrency-in-go.txt:831(搜「net/http/pprof」)
worker pool 收益同上text/15-ch05-5-concurrency-in-go.txt:855(搜「Worker Pools」)
AI 六步流程同上text/15-ch05-5-concurrency-in-go.txt:929(搜「Practical Guide」)

Footnotes

  1. 出处:「5. Concurrency in Go」第 53–81 段(text/15-ch05-5-concurrency-in-go.txt:64,搜「concurrency paradigm influenced」)。原文:「Go employs a concurrency paradigm influenced by Communicating Sequential Processes (CSP)」。 ↩

  2. 出处:同章第 82–90 段(text/15-ch05-5-concurrency-in-go.txt:84,搜「select statement enables」;sync、context 并列介绍)。 ↩

  3. 出处:同章第 623–646 段(text/15-ch05-5-concurrency-in-go.txt:639,搜「expected result might be 1000」;「behave correctly thousands of times」见 :609)。 ↩

  4. 出处:同章第 132–156 段(text/15-ch05-5-concurrency-in-go.txt:132,搜「Understanding Goroutines」;「few kilobytes」与「millions of goroutines」见 :136–138)。 ↩

  5. 出处:同章第 139–150 段(text/15-ch05-5-concurrency-in-go.txt:139,搜「G-M-P model」;work stealing 与异步抢占见 :144–146)。 ↩ ↩2 ↩3 ↩4

  6. 出处:同章第 158–174 段(text/15-ch05-5-concurrency-in-go.txt:159,搜「go keyword」;匿名 goroutine 见 :166)。 ↩

  7. 出处:同章第 175–193 段(text/15-ch05-5-concurrency-in-go.txt:175,搜「Loop Variable Capture」)。「The variable's value changes during the loop」为原文明写(:183)。(新版本 Go 每轮新变量是对该语义的修正,属书外进展,标为通用知识。) ↩ ↩2

  8. 出处:同章第 210–249 段(text/15-ch05-5-concurrency-in-go.txt:210,搜「Unbuffered Channels」;交棒比喻见 :248)。 ↩ ↩2 ↩3

  9. 出处:同章第 251–289 段(text/15-ch05-5-concurrency-in-go.txt:251,搜「Buffered Channels」;「Once the buffer is full, the behavior becomes synchronous」见 :256)。 ↩ ↩2

  10. 出处:同章第 332–342 段(text/15-ch05-5-concurrency-in-go.txt:333,搜「Backpressure refers to」)。 ↩ ↩2

  11. 出处:同章第 344–360 段(text/15-ch05-5-concurrency-in-go.txt:345,搜「Buffered channels always improve performance」)。 ↩

  12. 出处:同章第 382–411 段(text/15-ch05-5-concurrency-in-go.txt:382,搜「Producer–Consumer Coordination」)。 ↩

  13. 出处:同章第 413–453 段(text/15-ch05-5-concurrency-in-go.txt:413,搜「Pipeline Communication」;三 stage 代码见 :420)。 ↩

  14. 出处:同章第 455–494 段(text/15-ch05-5-concurrency-in-go.txt:455,搜「Fan-out and Fan-in」;三个 worker 的代码见 :466)。 ↩

  15. 出处:同章第 496–519 段(text/15-ch05-5-concurrency-in-go.txt:496,搜「Token channels」;「only 3 concurrent operations allowed」见 :504)。 ↩

  16. 出处:同章第 521–540 段(text/15-ch05-5-concurrency-in-go.txt:521,搜「Notifications and Signaling」)。 ↩

  17. 出处:同章第 542–570 段(text/15-ch05-5-concurrency-in-go.txt:542,搜「Avoiding Shared State」)。query/reply 代码见 :552。 ↩

  18. 出处:同章第 572–598 段(text/15-ch05-5-concurrency-in-go.txt:572,搜「Graceful Shutdown」;select+quit 代码见 :583)。 ↩ ↩2

  19. 出处:同章第 623–646 段(text/15-ch05-5-concurrency-in-go.txt:624,搜「A race condition exists when」;三条件见 :625,读-改-写见 :640)。 ↩ ↩2 ↩3

  20. 出处:同章第 648–662 段(text/15-ch05-5-concurrency-in-go.txt:648,搜「Overview of the Sync Package」)。 ↩

  21. 出处:同章第 664–680 段(text/15-ch05-5-concurrency-in-go.txt:664,搜「Using sync.Mutex」)。 ↩

  22. 出处:同章第 682–692 段(text/15-ch05-5-concurrency-in-go.txt:690,搜「defer mu.Unlock」)。 ↩

  23. 出处:同章第 693–699 段(text/15-ch05-5-concurrency-in-go.txt:693,搜「Mutex Granularity」)。 ↩ ↩2

  24. 出处:同章第 725–740 段(text/15-ch05-5-concurrency-in-go.txt:726,搜「not directly used for preventing data races」)。 ↩

  25. 出处:同章第 700–723 段(text/15-ch05-5-concurrency-in-go.txt:700,搜「Reader/Writer Locking」;starvation 见 :721)。 ↩ ↩2 ↩3

  26. 出处:同章第 787–800 段(text/15-ch05-5-concurrency-in-go.txt:787,搜「Happens-before」;四条规则见 :793)。 ↩ ↩2 ↩3

  27. 出处:同章第 824–843 段(text/15-ch05-5-concurrency-in-go.txt:824,搜「pprof—allow detailed sampling」)。 ↩

  28. 出处:同章第 844–853 段(text/15-ch05-5-concurrency-in-go.txt:844,搜「systemic patterns」)。 ↩

  29. 出处:同章第 855–884 段(text/15-ch05-5-concurrency-in-go.txt:855,搜「Worker Pools as a Concurrency」;四条收益见 :863)。 ↩

  30. 出处:同章第 929–956 段(text/15-ch05-5-concurrency-in-go.txt:929,搜「Practical Guide」;「top 20–50 lines」见 :955)。 ↩ ↩2

  31. 出处:同章第 958–968 段(text/15-ch05-5-concurrency-in-go.txt:959,搜「targeted prompts」)。六个问题示例均为原书原话(英文)。 ↩

  32. 出处:同章第 970–992 段(text/15-ch05-5-concurrency-in-go.txt:970,搜「Apply AI-Suggested Refactorings」)。 ↩

  33. 出处:同章第 994–1006 段(text/15-ch05-5-concurrency-in-go.txt:994,搜「Re-Profile and Validate」)。「AI optimization is hypothesis-driven. Verification is still grounded in measurement.」为原文明写。 ↩

On this page