一. channel 忘记关闭时问题演示
- 如下代码: 在do函数中,通过for + select 的模式,等待信道 taskCh 传递任务,并执行
func do(taskCh chan int) {
for {
select {
case t := <-taskCh:
time.Sleep(time.Millisecond)
fmt.Printf("task %d is done\n", t)
}
}
}
func sendTasks() {
taskCh := make(chan int, 10)
go do(taskCh)
for i := 0; i < 1000; i++ {
taskCh <- i
}
}
//运行测试
func TestDo(t *testing.T) {
t.Log(runtime.NumGoroutine())
sendTasks()
time.Sleep(time.Second)
t.Log(runtime.NumGoroutine())
}
- 执行单元测试后,会发现子协程多了一个,有一个协程一直没有得到释放: sendTasks 中启动了一个子协程 go do(taskCh),因为这个协程一直处于阻塞状态,等待接收任务,因此直到程序结束,协程也没有释放
- 如果任务全部发送成功,我们如何通知该协程结束等待,正常退出呢?
二. 如何通知协程结束等待正常退出
- 先复习一个channel的几种状态:
- 解决
- doCheckClose()函数中,增加"beforeClosed := <-taskCh" 判断 channel 是否已经关闭,beforeClosed 为 false 表示信道已被关闭。若关闭,则不再阻塞等待,直接返回,对应的协程随之退出。
- sendTasks 函数中,任务发送结束之后,使用 close(taskCh) 将 channel taskCh 关闭
- 后续启动的协程已经正常退出,该协程以及使用到的信道 taskCh 将被垃圾回收,资源得到释放
func doCheckClose(taskCh chan int) {
for {
select {
case t, beforeClosed := <-taskCh:
if !beforeClosed {
fmt.Println("taskCh has been closed")
return
}
time.Sleep(time.Millisecond)
fmt.Printf("task %d is done\n", t)
}
}
}
func sendTasksCheckClose() {
taskCh := make(chan int, 10)
go doCheckClose(taskCh)
for i := 0; i < 1000; i++ {
taskCh <- i
}
close(taskCh)
}
//运行测试
func TestDoCheckClose(t *testing.T) {
t.Log(runtime.NumGoroutine())
sendTasksCheckClose()
time.Sleep(time.Second)
runtime.GC()
t.Log(runtime.NumGoroutine())
}
三. 通道关闭原则
- 不推荐的直接关闭,如下:
func SafeClose(ch chan T) (justClosed bool) {
defer func() {
if recover() != nil {
// 一个函数的返回结果可以在defer调用中修改。
justClosed = false
}
}()
// 假设ch != nil。
close(ch) // 如果 ch 已关闭,将 panic
return true // <=> justClosed = true; return
}
- 使用 sync.Once 或互斥锁(sync.Mutex)确保 channel 只被关闭一次
type MyChannel struct {
C chan T
once sync.Once
}
func NewMyChannel() *MyChannel {
return &MyChannel{C: make(chan T)}
}
func (mc *MyChannel) SafeClose() {
mc.once.Do(func() {
close(mc.C)
})
}
- 优雅的方式:(参考channel文档)
情形一:M个接收者和一个发送者,发送者通过关闭用来传输数据的通道来传递发送结束信号。
情形二:一个接收者和N个发送者,此唯一接收者通过关闭一个额外的信号通道来通知发送者不要再发送数据了。
情形三:M个接收者和N个发送者,它们中的任何协程都可以让一个中间调解协程帮忙发出停止数据传送的信号。