前言
想必听说过 go
的,应该都知道 go
的最大的特性 goroutine
并发编程,而说到并发编程,使用 channel
进行数据传输是 go
中的必修课。
go
的并发哲学:不要通过共享内存来通信,而要通过通信来实现内存共享。
channel
的坑不少,本篇简单聊聊关闭 channel
的方法。
关闭 channel 的基本原则
坊间流传的关闭 channel
的原则:
不要从接收端关闭
channel
,也不要在有多个发送端时,主动关闭channel
这个原则的来源就因为:
- 不能向已关闭的
channel
发送数据- 不能重复关闭已关闭的
channel
如何关闭
- 比较粗暴的方式,使用
defer-recovery
机制,在关闭的时候如果panic
了,也会被recovery
。 - 既然
channel
只能close
一次,那么go
的源码包中的sync.Once
就可以派上用场了,专门做这种事情的。
接下来根据 sender
和 receiver
的个数,分如下几种情况:
- 单
sender
单receiver
- 单
sender
多receiver
- 多
sender
单receiver
- 多
sender
多receiver
第 1,2 种情况,直接在 sender
端关闭 channel
即可。
func main() {
dataCh := make(chan int, 100)
// sender
go func() {
for i := 0; i < 1000; i++ {
dataCh <- i + 1
}
log.Println("send complete")
close(dataCh)
}()
// receiver
for i := 0; i < 5; i++ {
go func() {
for {
data, ok := <-dataCh
if !ok { // 已关闭
return
}
_ = data
}
}()
}
select {
case <-time.After(time.Second * 5):
fmt.Println(runtime.NumGoroutine())
}
}
第 3 种情况,可以增加一个传递关闭信号的 stopCh
,在 receiver
端通过 stopCh
下达关闭数据 dataCh
的指令。sender
监听到关闭信号后,不再向数据 dataCh
发送数据。
package main
import (
"time"
"math/rand"
"sync"
"log"
)
func main() {
rand.Seed(time.Now().UnixNano())
log.SetFlags(0)
const Max = 100000
const NumSenders = 1000
wgReceivers := sync.WaitGroup{}
wgReceivers.Add(1)
dataCh := make(chan int)
stopCh := make(chan struct{})
// senders
for i := 0; i < NumSenders; i++ {
go func() {
for {
select {
case <- stopCh:
return
default:
}
select {
case <- stopCh:
return
case dataCh <- rand.Intn(Max):
}
}
}()
}
// receiver
go func() {
defer wgReceivers.Done()
for value := range dataCh {
if value == Max-1 {
close(stopCh)
return
}
log.Println(value)
}
}()
wgReceivers.Wait()
}
第 4 种情更为复杂一点,不能够像第 3 种情况那样直接在 receiver
端关闭 stopCh
,这样会导致重复关闭已关闭的 channel
而 panic
。因此需要再加个中间人 toStop
来接收关闭 stopCh
的请求。
package main
import (
"time"
"math/rand"
"sync"
"log"
"strconv"
)
func main() {
rand.Seed(time.Now().UnixNano())
log.SetFlags(0)
const Max = 100000
const NumReceivers = 10
const NumSenders = 1000
wgReceivers := sync.WaitGroup{}
wgReceivers.Add(NumReceivers)
dataCh := make(chan int)
stopCh := make(chan struct{})
// 这个是添加的中间人,通过它来接收关闭 stopCh 的请求,做一次关闭
// 这里给缓存是 goroutine 启动时机,可能导致 select 选择,导致逻辑问题
toStop := make(chan string, 1)
var stoppedBy string
go func() {
stoppedBy = <-toStop
close(stopCh)
}()
// senders
for i := 0; i < NumSenders; i++ {
go func(id string) {
for {
value := rand.Intn(Max)
if value == 0 {
select {
case toStop <- "sender#" + id:
default:
}
return
}
// 由于 select 是随机选择的,所以先在这里尝试得知是否关闭
select {
case <- stopCh:
return
default:
}
select {
case <- stopCh:
return
case dataCh <- value:
}
}
}(strconv.Itoa(i))
}
// receivers
for i := 0; i < NumReceivers; i++ {
go func(id string) {
defer wgReceivers.Done()
for {
select {
case <- stopCh:
return
default:
}
select {
case <- stopCh:
return
case value := <-dataCh:
if value == Max-1 {
select {
case toStop <- "receiver#" + id:
default:
}
return
}
log.Println(value)
}
}
}(strconv.Itoa(i))
}
wgReceivers.Wait()
log.Println("stopped by", stoppedBy)
}
这个例子可以在 sender
和 receiver
端都发送关闭信号,通过 toStop
这个中间人来传递关闭信号,接收到之后关闭 stopCh
。这里需要注意将 toStop
定义为带缓冲的 channel
,若是不带缓冲,可能会出现 <-toStop
这个接收协程还未跑起来时,就已经有其他协程向其发送了 toStop<-xx
关闭信号。
这时在 sender
或 receiver
的 select
分支就可能走 default
语句,导致逻辑错误。
这个例子中,简单点的做法可以给 toStop
设置缓存为 sender
与 receiver
的和,就可以简写为如下:
...
toStop := make(chan string, NumReceivers + NumSenders)
...
value := rand.Intn(Max)
if value == 0 {
toStop <- "sender#" + id
return
}
...
if value == Max-1 {
toStop <- "receiver#" + id
return
}
...
channel 的注意点
channel
的声明必须使用make
关键字,不能直接var c chan int
,这样得到的是nil channel
- 不能向
nil channel
发送数据
var c chan int
c <- 1 // panic
- 已关闭的
channel
不能再往其发送数据
c := make(chan int)
close(c)
c <- 1 // panic
- 不能重复关闭已关闭的
channel
c := make(chan int)
close(c)
close(c) // panic
- 只要你的
channel
没有引用关系了,就算你没有close
关闭或者channel
有大量的堆积数据没有消费,最终都会被gc
释放。
总结
关闭 channel
的基本法则:
- 单
sender
的情况下,都可以直接在sender
端关闭channel
。 - 多
sender
的情况下,可以增加一个传递关闭信号的channel
专门用于关闭数据传输的channel
。
原则:不要从接收端关闭 channel
,也不要在有多个发送端时,主动关闭 channel
。
本质:已关闭的 channel
不能再关闭(或者再向其发送数据)。
channel
的使用非常多样,本篇列举了几个基本的场景,真正想要融会贯通,还是得要多写多思考。