1. 前言

你好哇!本文是「Golang 并发编程」系列的第 3 篇文章~

上篇文章我们学习了 channel 的基础用法,还不熟悉的朋友可以先看看上篇文章。

在使用 channel 进行 goroutine 之间的通信时,有时候场面会变得十分复杂,以至于写出难以觉察、难以定位的偶现 bug,而且上线的时候往往跑得好好的,直到某一天深夜收到服务挂了、OOM 了之类的告警……

本文来梳理一下使用 channel 中常见的三大坑:panic、死锁、内存泄漏,做到防患于未然。

2. 死锁

go 语言新手在编译时很容易碰到这个死锁的问题:

这个就是喜闻乐见的「死锁」了…… 在操作系统中,我们学过,「死锁」就是两个线程互相等待,耗在那里,最后程序不得不终止。go 语言中的「死锁」也是类似的,两个 goroutine 互相等待,导致程序耗在那里,无法继续跑下去。看了很多死锁的案例后,channel 导致的死锁可以归纳为以下几类案例(先讨论 unbuffered channel 的情况):

2.1 只有生产者,没有消费者,或者反过来

channel 的生产者和消费者必须成对出现,如果缺乏一个,就会造成死锁,例如:

或是:

2.2 生产者和消费者出现在同一个 goroutine 中

除了需要成对出现,还需要出现在不同的 goroutine 中,例如:

对于 buffered channel 则是:

2.3 buffered channel 已满,且出现上述情况

hchanringbuffer

所以实际使用中,推荐尽量使用 buffered channel ,使用起来会更安全,在下文的「内存泄漏」相关内容也会提及

3. 内存泄漏

OOM(Out of Memory)

在 go 语言中,错误地使用 channel 会导致 goroutine 泄漏,进而导致内存泄漏。

3.1 如何实现 goroutine 泄漏呢?

不会修 bug,我还不会写 bug 吗?让 goroutine 泄漏的核心就是:

生产者/消费者 所在的 goroutine 已经退出,而其对应的 消费者/生产者 所在的 goroutine 会永远阻塞住,直到进程退出

3.2 生产者阻塞导致泄漏

我们一般会用 channel 来做一些超时控制,例如下面这个例子:

g1g2
g2ch
g1
  1. 假设客户端超时调整为 5000ms,实际请求耗时 2s,则 select 会进入获取 result 的分支,输出如下:

3.3 消费者阻塞导致泄漏

如果生产者不继续生产,消费者所在的 goroutine 也会阻塞住,不会退出,例如:

close(ch)for-range

3.4 如何预防内存泄漏?

预防 goroutine 泄漏的核心就是:

创建 goroutine 时就要想清楚它什么时候被回收

具体到执行层面,包括:

buffered channelbuffered channel

4. panic

panic 就更刺激了,一般是测试的时候没发现,上线之后偶现,程序挂掉,服务出现一个超时毛刺后触发告警。channel 导致的 panic 一般是以下几个原因:

4.1 向已经 close 掉的 channel 继续发送数据

先举一个简单的栗子:

在实际开发过程中,处理多个 goroutine 之间协作时,可能存在一个 goroutine 已经 close 掉 channel 了,另外一个不知道,也去 close 一下,就会 panic 掉,例如:

万恶之源就是在 go 语言里,你是无法知道一个 channel 是否已经被 close 掉的,所以在尝试做 close 操作的时候,就应该做好会 panic 的准备……

4.2 多次 close 同一个 channel

同上,在尝试往 channel 里发送数据时,就应该考虑

  • 这个 channel 已经关了吗?
  • 这个 channel 什么时候、在哪个 goroutine 里关呢?
  • 谁来关呢?还是干脆不关?

5. 如何优雅地 close channel

5.1 我们需要检查 channel 是否关闭吗?

closed
closed(ch)
closed

5.2 需要 close 吗?为什么?

结论:除非必须关闭 chan,否则不要主动关闭。关闭 chan 最优雅的方式,就是不要关闭 chan~

当一个 chan 没有 sender 和 receiver 时,即不再被使用时,GC 会在一段时间后标记、清理掉这个 chan。那么什么时候必须关闭 chan 呢?比较常见的是将 close 作为一种通知机制,尤其是生产者与消费者之间是 1:M 的关系时,通过 close 告诉下游:我收工了,你们别读了。

5.3 谁来关?

chan 关闭的原则:

  1. Don't close a channel from the receiver side 不要在消费者端关闭 chan
  2. Don't close a channel if the channel has multiple concurrent senders 有多个并发写的生产者时也别关


只要我们遵循这两条原则,就能避免两种 panic 的场景,即:向 closed chan 发送数据,或者是 close 一个 closed chan。

按照生产者和消费者的关系可以拆解成以下几类情况:

try-send

本用例来自参考资料中的《How to Gracefully Close Channels》,go 101 系列非常不错