go channel 基础(如何优雅地关闭 channel)

前言

想必听说过 go 的,应该都知道 go 的最大的特性 goroutine 并发编程,而说到并发编程,使用 channel 进行数据传输是 go 中的必修课。

go 的并发哲学:不要通过共享内存来通信,而要通过通信来实现内存共享。

channel 的坑不少,本篇简单聊聊关闭 channel 的方法。

关闭 channel 的基本原则

坊间流传的关闭 channel 的原则:

不要从接收端关闭 channel,也不要在有多个发送端时,主动关闭 channel

这个原则的来源就因为:

  1. 不能向已关闭的 channel 发送数据
  2. 不能重复关闭已关闭的 channel

如何关闭

  1. 比较粗暴的方式,使用 defer-recovery 机制,在关闭的时候如果 panic 了,也会被 recovery
  2. 既然 channel 只能 close 一次,那么 go 的源码包中的 sync.Once 就可以派上用场了,专门做这种事情的。

接下来根据 senderreceiver 的个数,分如下几种情况:

  1. senderreceiver
  2. senderreceiver
  3. senderreceiver
  4. senderreceiver

第 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,这样会导致重复关闭已关闭的 channelpanic。因此需要再加个中间人 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)
}

这个例子可以在 senderreceiver 端都发送关闭信号,通过 toStop 这个中间人来传递关闭信号,接收到之后关闭 stopCh。这里需要注意将 toStop 定义为带缓冲的 channel,若是不带缓冲,可能会出现 <-toStop 这个接收协程还未跑起来时,就已经有其他协程向其发送了 toStop<-xx 关闭信号。
这时在 senderreceiverselect 分支就可能走 default 语句,导致逻辑错误。

这个例子中,简单点的做法可以给 toStop 设置缓存为 senderreceiver 的和,就可以简写为如下:

...
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 的使用非常多样,本篇列举了几个基本的场景,真正想要融会贯通,还是得要多写多思考。

参考

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值