编程语言研究书库

channel 与互斥锁:goroutine 之间怎么说话、怎么不打架

channel 与互斥锁:goroutine 之间怎么说话、怎么不打架

上一章开了成千上万个 goroutine,这一章教它们两件事:怎么互相传话(channel), 以及怎么不抢同一块内存(mutex)。 Go 有句名言,书里原文引了: 「不要通过共享内存来通信,要通过通信来共享内存。」1 前半句是 mutex 的领地,后半句是 channel 的领地——本章两半各讲一头。

1. channel:一条有规矩的管道

channel(通道)是 goroutine 之间传值的管道,用 make 创建,天生带类型。

ch := make(chan int)        // 能传 int 的管道
var ch1 chan int            // 声明而不 make:这是 nil channel

两个出生方式有天壤之别,书里强调过:nil channel 上发送或接收会永远阻塞; 所以 channel 几乎总是用 make 造2。

管道上只有两个动作,符号都在第 5 章的杂项运算符里见过:

ch <- 8      // 发送:把 8 推进管道
v := <-ch    // 接收:从管道取一个值出来
v, ok := <-ch // 接收 + 状态:ok 告诉你这次接收成没成

核心魔法,也是核心纪律:发送和接收互相等

书里给的发送定义藏着全章最重要的机制: 发送操作会阻塞(停在原地),直到有接收方准备好接这个值;接收同理, 没有值发过来就一直等。3

主线例子的第一站:一个发送,没有接收——

func main() {
    ch := make(chan int)
    ch <- 8          // 停在这里,永远等不到人来接
}

程序不是报错退出,而是被运行时报死锁(deadlock):所有 goroutine 都睡着了。 书里给了完整的报错原文:fatal error: all goroutines are asleep – deadlock!, 并标注了卡在哪一步(goroutine 1 [ chan send ])4。

图说:死锁 = 至少一个 goroutine 被阻塞,且再没有别的 goroutine 能救它。 发送等接收、接收等发送,全等睡着了,运行时就拉响警报。

配套的正确姿势:发送放进 goroutine,接收留在 main——

func send(c chan int) { c <- 8 }

func main() {
    c := make(chan int)
    go send(c)            // 发送者在 goroutine 里等
    a, ok := <-c          // main 来接 → 两边对上,都放行
    // → a=8, ok=true
}

ok 的约定和 map 的逗号 ok、接口的类型断言是同一个家族: channel 被关闭且掏空之后,再接收,ok = false(见第 4 节)5。

2. 无缓冲与缓冲:等不等人的差别

默认造的 channel 是无缓冲的:发送必须当场有人接,一手交钱一手交货。 缓冲 channel 给管道加了个蓄水池:make(chan int, 2) 表示可以先存 2 个值, 没人接也不堵;第 3 个才堵。6

ch := make(chan int, 2)
ch <- 8
ch <- 99        // 两个都存下了,不等人
a := <-ch       // → 8(先进先出)
b := <-ch       // → 99

cap(ch) 取容量:容量 2 的 channel,塞满 2 个值之后第 3 个发送照样阻塞7。 蓄水池多大合适是并发设计的真实权衡——书里没有展开,记住「缓冲只是把 「当场交货」放宽成「可以欠着」即可。

3. 多个 goroutine 用管道分工:主线例子第二站

这是本章主线的中间站,也是「通信共享内存」的正脸演示8:

func square(input chan int, output chan int) {
    value := <-input
    output <- value * value
}
func cube(input chan int, output chan int) {
    value := <-input
    output <- value * value * value
}

func main() {
    num := make(chan int)
    sqrOut := make(chan int)
    cubeOut := make(chan int)

    go square(num, sqrOut)     // 两个工人各守一条管道
    go cube(num, cubeOut)

    num <- 3                   // 给 square 派活
    sqrVal := <-sqrOut         // → 9
    num <- 3                   // 给 cube 派活
    cubeVal := <-cubeOut       // → 27
}

图说:两个 goroutine 没有共享任何变量,只靠管道收活、交活—— 这就是那句名言的完整形状。没有锁,因为管道本身的「等」就是同步。

4. close:管道的下班铃

close(ch) 宣布:这个 channel 不会再有发送了。 它牵出两条规矩,书里都用实验证明过9:

  1. 往已关闭的 channel 发送 → 运行时 panic(panic: send on closed channel);
  2. 从已关闭的 channel 接收 → 不报错,返回零值,ok = false。

书里演示的事故顺序值得记住:发数 → 还没收就先 close → 接收拿到 0, false → 随后 goroutine 里那次发送撞上关闭的 channel, panic9。close 由发送方负责,且只能 close 一次——接收方靠 ok 判断收尾, 这是遍历 channel 的 for-range 能干净退出的底层机制(补充,不在书里)。

5. 单向 channel:把规矩写进类型

默认 channel 双向(能发能收)。可以给函数的参数「定向」,把规矩写进签名:

写法含义
chan<- int只能发(send-only):函数往里塞值
<-chan int只能收(receive-only):函数从里取值

书里给的管道流水线:producer 拿 chan<- int 只管发, consumer 拿 <-chan int 只管收,编译器把「反向使用」直接判成编译错 (Cannot use value <type chan<- int> as type int in send)10。

双向 channel 可以自动转成单向传给函数——这是惯例:造的管道是双向的, 传给每个函数时按职责收窄。方向收窄是编译期的纪律,不加锁也有用。

6. select:同时等好几条管道

select 同时等多个 channel 操作,哪个先就绪就走哪个分支; 一个都没就绪就阻塞,可以加 default 变成「不阻塞、没就绪就走默认」。11

三个规则,书里各给了一个实验:

规则一:只执行一个分支。 square(睡 5 秒)和 cube(睡 10 秒)同时起跑, select 等到 5 秒的那头,打出 square of a value is: 9 就离场—— cube 的结果它不管(书里也没有再等 cube)12。

规则二:多个分支同时就绪,随机挑一个。 两边都睡 1 秒时,「运行多次,输出不同」:这次 Cube of a value is: 125, 下次可能是 square of a value is: 913。select 的随机是刻意的: 防止程序员依赖某个顺序。

规则三:一个就绪的都没有 → 阻塞,全程序睡着 → 死锁。 两个接收分支都没值来,运行时照样拉警报; select{}(一个 case 都没有)同样立即死锁14。

default 分支让 select 变成「看一眼就走」:

select {
case v := <-ch1: ...
case v := <-ch2: ...
default:
    fmt.Println("No case statement is ready to execute!")   // 没就绪也不等
}

输出照原书:No case statement is ready to execute!15。 想要所有分支都被处理到,就把 select 放进循环里——书里的最终版 用 for itr := 1; itr <= 2; itr++ 包住 select,先收 9、再收 125,两个都拿到16。

7. 数据竞争:当两个 goroutine 抢同一个变量

channel 之外,还有共享内存的老路——而数据竞争(data race)是它的暗礁。 书里给的定义:两个或多个线程同时访问同一内存位置,且至少一个是写17。

主线例子第三站:两个 goroutine 都执行这个函数,共享全局变量 value = 45:

func update_value() {
    if value == 45 {
        value++
    }
    fmt.Println(value)
    wg.Done()
}

书里把三种交错(谁在什么时候读写)排了出来,这是全章最有教学价值的一段18:

  情况一(顺序执行,正确):
    gr1 读45 → gr1 写46 → gr2 读46 → gr2 不写了
    输出:46 46

  情况二(都先读后写,出错):
    gr1 读45 → gr2 读45 → gr1 写46 → gr2 写46
    输出:46 46   ← 两个都加了一次?不,各自写的是46,加总共少了
    注:更糟的交错是两次 ++ 只生效一次,书上以「unexpected result」概括

  情况三(顺序换一换,同样出错):
    gr1 读45 → gr2 读45 → gr2 写46 → gr1 写46

图说:if value == 45 { value++ } 看着是「安全」的两步,并发下它是三步 (读、判、写),三步之间任何人都能插进来。代码层面没有「原子」这回事, 除非你主动给它加锁。

竞态条件 ≠ 数据竞争

书里特意把两个词分开:race condition(竞态条件)是「操作的时序影响结果」 的程序特性;data race 是「未同步地同时读写」的内存访问错误。 有竞态条件不一定有数据竞争——它给的例子是两个线程都加锁再写: 没有数据竞争(锁保证了不同时写),但最终值是 1 还是 2 仍取决于谁先谁后, 竞态条件依然存在19。这个区分很细,但值得记:锁解决 data race, 不解决竞态条件;后者要靠设计(比如规定顺序、幂等操作)消化。

抓现行:race detector

Go 工具链自带数据竞争探测器:go run -race program.go(或 go test -race)。 它跟踪所有内存访问的同步情况,发现未同步的共享访问就打印警告。20

书里给的使用纪律非常实用:探测器让 CPU 和内存开销涨到十倍左右, 别常态开着,怀疑有竞争时再开20。

8. Mutex:银行账户的三连改,三个真实的坑

Mutex(互斥锁)是 sync 包里的锁:同一时刻只允许一个 goroutine 持有; Lock() 抢锁,Unlock() 放锁。 惯用法书里给了:用 defer Unlock, 保证函数任何出口都放锁(呼应第 6 章的 defer)21。

主线例子的压轴:一个带锁的账户 initial_amount{amount, sync.Mutex}, main 里开 10 轮「存 500 + 取 300」的 goroutine。书里把这个例子改了三个版本, 每个版本修一个坑——这是全书最像真实调试的一段22:

第一版:存取都加锁,但读余额没等 goroutine 跑完。

for ... { go ia.deposit_amount(500); go ia.withdraw_amount(300) }
final := ia.get_balance()     // 没等!goroutine 还在跑
// → Final balance is: 3900   ← 每次运行都可能不一样

3900 是半成品时刻的余额。坑一:WaitGroup 没用,读得太早。

第二版:给 get_balance 也加锁,再配 WaitGroup。

// → Final balance is: 4300

锁加对了,为什么还是错?书里点破:这次运行还提示了一个 data race 警告 (get_balance 读的时候,写锁可能正被持有——读也要加锁); 修完警告后得到 4300——比 3900 进步了,但正确答案 2000+2000−3000=4000 还没到。 坑二(微妙的那个):WaitGroup 的 Add 放在循环里、时机不对,Wait 返回时 仍有 goroutine 没交卷——读到的仍是中间值。

第三版:Add 与 go 配对、每个 goroutine 都 Done、读之前 Wait。

for itr := 1; itr <= 10; itr++ {
    wg.Add(1); go ia.deposit_amount(500)
    wg.Add(1); go ia.withdraw_amount(300)
}
wg.Wait()
final := ia.get_balance()
// → final balance is: 4000   ← 终于对了

图说:三个版本 3900 → 4300 → 4000,踩的坑依次是 「没等跑完」「读没加锁/等待不完整」「配对正确」。 并发程序的正确性,锁只占一半;另一半是「在正确的时刻读」。

判断(我们的,不是书里的): 银行账户这个三连改,是全书最有价值的一段代码, 因为它演示了并发 bug 的典型形态:每次运行答案都不一样,而且每个答案都「看着合理」。 单线程的直觉(跑了三遍都对)在并发下完全失效——必须把「为什么对」讲成 交错时间表,才算对。这也是为什么 -race 和 WaitGroup 是并发入门的第一件行李。 如果错,会错在: channel 派会主张「用 channel 把账户包起来,就不必修锁」—— 这是风格之争(书里也承认 channel 有性能代价);但「等待时机」这个坑, 换 channel 一样存在,不改变判断。

9. RWMutex:读多写少的锁

RWMutex(读写锁)把读和写分开:多个读者可以同时持有读锁(RLock), 写者独占(Lock);有写者在等时,新来的读者要排队。

方法谁用规矩
RLock / RUnlock只读可以多人同时
Lock / Unlock写独占;别人读或写都阻塞

书里给的场景:一个 map 被嵌入 struct,update_map 用写锁, read_map 用读锁——并发读写不再撞车23。

两个使用纪律,书里各提了一句,都很要紧:

  • 别在同一个 struct 里同时嵌入 Mutex 和 RWMutex,编译器报 「ambiguous selector」(两者的 Lock 提升后冲突);
  • RWMutex 比 Mutex 重:读远多于写时才用它,写多读少反而更慢。

10. 作者的判断与证据

书里的说法性质
「不要通过共享内存通信,要通信共享内存」Go 官方口号,书里引用(第 484 段原文)
「channel 和 Mutex 各有优劣,看场景」罕见的中立,属实
「race detector 开销十倍,必要时才开」属实,且是实用建议

11. 边界与局限

  • for-range 遍历 channel(发送方 close、接收方 range 干净退出) 这个最重要的惯用法没有讲,只用了「ok 模式」铺垫;
  • context 包的取消机制没有提——真实服务的 goroutine 治理靠它;
  • select 放进 for 循环里的经典形态(配 close 退出)未给出完整范式;
  • 「锁的粒度」只说了一句性能代价,没给具体取舍方法。

12. 可带走的

  1. channel 用 make 造;nil channel 收发永远阻塞;
  2. 发送等接收、接收等发送——「等」就是 channel 的同步,死锁就是它惹的祸;
  3. 接收用 v, ok := <-ch:close 并掏空后 ok = false;
  4. 无缓冲 = 一手交货;缓冲 = 可欠 capacity 个;cap 取容量;
  5. 多 goroutine 分工用多条管道收活交活,不共享变量;
  6. close 由发送方负责;发向已关闭的 channel = panic,收则零值+false;
  7. 单向 channel(chan<- / <-chan)把收发规矩写进函数签名,双向可自动转单向;
  8. select 同时等多条管道:谁就绪走谁,都就绪随机挑,都不就绪阻塞(或走 default);全都要处理就把 select 放循环里;
  9. 数据竞争 = 同时访问同一内存且至少一方写;交错时间表是分析它的唯一办法;
  10. 竞态条件 ≠ 数据竞争:锁治后者,前者靠设计;
  11. 怀疑有竞争,go run -race 抓现行(开销十倍,别常态开);
  12. Mutex:Lock 后 defer Unlock;账户例子的三个坑——没等跑完、读没加锁、Add 时机——对应 3900/4300/4000;
  13. 读多写少用 RWMutex(读者并行、写者独占),别和 Mutex 同时嵌入一个 struct。

13. 原文地图

主题原书章原文位置
var 与 make 声明 channel、nil channelMutex and Channelstext/24-fm-learning-go-programming.txt:64(搜「nil channel」) · text/24-fm-learning-go-programming.txt:106(搜「block forever」)
发送阻塞、两个值的接收与 okMutex and Channelstext/24-fm-learning-go-programming.txt:115(搜「blocks the channels」) · text/24-fm-learning-go-programming.txt:136(搜「status of whether an operation is successful」)
无缓冲 vs 缓冲、capMutex and Channelstext/24-fm-learning-go-programming.txt:235(搜「unbuffered」) · text/24-fm-learning-go-programming.txt:238(搜「capacity of a channel」) · text/24-fm-learning-go-programming.txt:361(搜「2 value received」)
死锁(发送阻塞/接收阻塞)Mutex and Channelstext/24-fm-learning-go-programming.txt:370(搜「all the Goroutines are asleep」) · text/24-fm-learning-go-programming.txt:421(搜「fatal error」) · text/24-fm-learning-go-programming.txt:475(搜「chan receive」)
共享内存的名言、square/cube 管道分工Mutex and Channelstext/24-fm-learning-go-programming.txt:484(搜「Do not communicate by sharing memory」) · text/24-fm-learning-go-programming.txt:568(搜「Square of value: 9」)
close 与「send on closed channel」Mutex and Channelstext/24-fm-learning-go-programming.txt:595(搜「no value will be sent」) · text/24-fm-learning-go-programming.txt:688(搜「send on closed channel」) · text/24-fm-learning-go-programming.txt:691(搜「zero without any error」)
单向 channelMutex and Channelstext/24-fm-learning-go-programming.txt:697(搜「bidirectional」) · text/24-fm-learning-go-programming.txt:838(搜「Cannot use value」) · text/24-fm-learning-go-programming.txt:898(搜「convert a bidirectional channel」)
select 的规则与四个实验Mutex and Channelstext/24-fm-learning-go-programming.txt:964(搜「wait on multiple channel」) · text/24-fm-learning-go-programming.txt:1096(搜「square of a value is: 9」) · text/24-fm-learning-go-programming.txt:1201(搜「Cube of a value is: 125」) · text/24-fm-learning-go-programming.txt:1300(搜「asleep」) · text/24-fm-learning-go-programming.txt:1402(搜「No case statement」) · text/24-fm-learning-go-programming.txt:1429(搜「select <no cases>」) · text/24-fm-learning-go-programming.txt:1537(搜「9」与「125」两次收取)
数据竞争与三种交错Mutex and Channelstext/24-fm-learning-go-programming.txt:1552(搜「at least one write」) · text/24-fm-learning-go-programming.txt:1609(搜「three cases」)
竞态条件 ≠ 数据竞争Mutex and Channelstext/24-fm-learning-go-programming.txt:1723(搜「different from a data race」) · text/24-fm-learning-go-programming.txt:1747(搜「final value of a variable can be 1 or 2」)
race detector 与 -raceMutex and Channelstext/24-fm-learning-go-programming.txt:1753(搜「data race detector」) · text/24-fm-learning-go-programming.txt:1771(搜「ten times the CPU」)
Mutex 与银行账户三连改Mutex and Channelstext/24-fm-learning-go-programming.txt:1777(搜「mutual exclusion lock」) · text/24-fm-learning-go-programming.txt:1888(搜「3900」) · text/24-fm-learning-go-programming.txt:1894(搜「data race warning」) · text/24-fm-learning-go-programming.txt:1918(搜「4300」) · text/24-fm-learning-go-programming.txt:2050(搜「final balance is: 4000」)
RWMutexMutex and Channelstext/24-fm-learning-go-programming.txt:2056(搜「reader/writer Mutex」) · text/24-fm-learning-go-programming.txt:2071(搜「ambiguous selector」) · text/24-fm-learning-go-programming.txt:2188(搜「map[1:10 2:20 3:30 4:40 5:50 6:60 7:70 8:80 9:90」)

Footnotes

  1. 出处:「Mutex and Channels」第 484 段(text/24-fm-learning-go-programming.txt:484,搜「Do not communicate by sharing memory」)。 ↩

  2. 出处:「Mutex and Channels」第 64–106 段(text/24-fm-learning-go-programming.txt:103,搜「<nil> channel」;text/24-fm-learning-go-programming.txt:106,搜「block forever」)。 ↩

  3. 出处:「Mutex and Channels」第 115 段(text/24-fm-learning-go-programming.txt:115,搜「blocks the channels」)。 ↩

  4. 出处:「Mutex and Channels」第 121–133、364–427 段(text/24-fm-learning-go-programming.txt:421,搜「fatal error」;text/24-fm-learning-go-programming.txt:424,搜「chan send」)。 ↩

  5. 出处:「Mutex and Channels」第 136–229 段(text/24-fm-learning-go-programming.txt:181,搜「value received from channel: 8」;text/24-fm-learning-go-programming.txt:229,搜「chan send <nil chan>」)。 ↩

  6. 出处:「Mutex and Channels」第 232–310 段(text/24-fm-learning-go-programming.txt:238,搜「capacity of a channel」;text/24-fm-learning-go-programming.txt:310,搜「8 99」)。 ↩

  7. 出处:「Mutex and Channels」第 322–361 段(text/24-fm-learning-go-programming.txt:358,搜「Capacity of channel is: 2」)。 ↩

  8. 出处:「Mutex and Channels」第 481–590 段(text/24-fm-learning-go-programming.txt:568,搜「Square of value: 9」;text/24-fm-learning-go-programming.txt:571,搜「Cube of value: 27」)。 ↩

  9. 出处:「Mutex and Channels」第 592–691 段(text/24-fm-learning-go-programming.txt:688,搜「send on closed channel」;text/24-fm-learning-go-programming.txt:691,搜「zero without any error」)。 ↩ ↩2

  10. 出处:「Mutex and Channels」第 694–958 段(text/24-fm-learning-go-programming.txt:838,搜「Cannot use value」;text/24-fm-learning-go-programming.txt:898,搜「convert a bidirectional channel」)。 ↩

  11. 出处:「Mutex and Channels」第 961–964 段(text/24-fm-learning-go-programming.txt:964,搜「wait on multiple channel」)。 ↩

  12. 出处:「Mutex and Channels」第 1000–1099 段(text/24-fm-learning-go-programming.txt:1096,搜「square of a value is: 9」)。 ↩

  13. 出处:「Mutex and Channels」第 1102–1207 段(text/24-fm-learning-go-programming.txt:1201,搜「Cube of a value is: 125」;text/24-fm-learning-go-programming.txt:1204,搜「Or square」)。 ↩

  14. 出处:「Mutex and Channels」第 1210–1432 段(text/24-fm-learning-go-programming.txt:1300,搜「asleep」;text/24-fm-learning-go-programming.txt:1429,搜「select <no cases>」)。 ↩

  15. 出处:「Mutex and Channels」第 1309–1402 段(text/24-fm-learning-go-programming.txt:1402,搜「No case statement」)。 ↩

  16. 出处:「Mutex and Channels」第 1435–1543 段(text/24-fm-learning-go-programming.txt:1537,搜「square of a value is: 9」;text/24-fm-learning-go-programming.txt:1540,搜「Cube of a value is: 125」)。 ↩

  17. 出处:「Mutex and Channels」第 1552 段(text/24-fm-learning-go-programming.txt:1552,搜「at least one write」)。 ↩

  18. 出处:「Mutex and Channels」第 1609–1719 段(三种交错的图示见第 1625–1712 段;第 1609 段,搜「three cases」)。 ↩

  19. 出处:「Mutex and Channels」第 1720–1748 段(text/24-fm-learning-go-programming.txt:1723,搜「different from a data race」;text/24-fm-learning-go-programming.txt:1747,搜「final value of a variable can be 1 or 2」)。 ↩

  20. 出处:「Mutex and Channels」第 1750–1771 段(text/24-fm-learning-go-programming.txt:1753,搜「data race detector」;text/24-fm-learning-go-programming.txt:1771,搜「ten times the CPU」)。 ↩ ↩2

  21. 出处:「Mutex and Channels」第 1774–1891 段(text/24-fm-learning-go-programming.txt:1816,搜「withdraw_amount」;text/24-fm-learning-go-programming.txt:1825,搜「defer ia.Unlock()」;text/24-fm-learning-go-programming.txt:1888,搜「3900」)。 ↩

  22. 出处:「Mutex and Channels」第 1894–2050 段(三个版本:3900 见第 1888 段;4300 见第 1918 段及其前的 data race 警告分析;4000 见第 2050 段)。 ↩

  23. 出处:「Mutex and Channels」第 2053–2191 段(text/24-fm-learning-go-programming.txt:2071,搜「ambiguous selector」;text/24-fm-learning-go-programming.txt:2188,搜「9:90」)。 ↩

On this page