在go中,channel的使用频率很高,那么你知道关于channel关闭的知识吗?
为什么要关闭?不关闭会有什么风险?
在某些情况下,不关闭 channel 就会造成内存泄漏。
例如:channel 的发送次数小于接收次数时,接收者 go routine 由于等待发送者发送一直阻塞。因此接收者 go routine 一直未退出,channel内的数据也由于一直被接收者使用无法被垃圾回收。未退出的 go routine 和未被回收的 channel 都造成了内存泄漏的问题。
当 channel 的发送次数大于接收次数时,也会出现同样的问题导致内存泄漏。
补充关于内存回收的两点:
- 当一个 Goroutine 退出时,它所使用的内存资源会被回收。如果 Goroutine 中创建了其他资源(如 Channel、互斥锁等),这些资源也会被一并回收。
- 当一个 Channel 被关闭后,如果没有任何 Goroutine 持有该 Channel 的引用,Channel 占用的内存资源会被回收。
另外,并不是所有情况都需要关闭channel:
当 channel 的发送和接收次数确定且相同时,发送者 go routine 和接收者 go routine 分别都会在发送或接收结束时结束各自的 go routine。channel中的数据也会由于没有代码使用被垃圾收集器回收。因此这种情况下,不关闭 channel,没有任何副作用。
总结下来,当 channel 的发送和接收次数确定且相同时,不需要关闭。channel 的发送次数不确定时,需要关闭。
如何优雅地关闭
首先要遵循以下两点规律:
- 应该只在发送端关闭 channel。(防止关闭后继续发送)
- 存在多个发送者时不要关闭发送者 channel,而是使用专门的 stop channel。(因为多个发送者都在发送,且不可能同时关闭多个发送者,否则会造成重复关闭。发送者和接收者多对一时,接收者关闭 stop channel;多对多时,由任意一方关闭 stop channel,双方监听 stop channel 终止后及时停止发送和接收)
另外,golang 官方为我们提供了一种方式,可以用来尽量避免这个问题。golang 允许我们在把 channel 作为参数时,使用 <- 控制 channel 在函数内只读或只写,防止我们在错误的时候关闭 channel。
func TestOneSenderOneReceiver(t *testing.T) {
ich := make(chan int)
go sender(ich)
go receiver(ich)
}
// 发送者
func sender(ich chan<- int) { // 注意参数中的箭头
for i := 0; i < 100; i++ {
ich <- i
}
}
// 接收者
func receiver(ich <-chan int) { // 注意参数中的箭头
fmt.Println(<-ich)
close(ich) // 此处代码会在编译期报错
}
有缓存通道与无缓存通道的区别
创建方式
无缓冲通道
unbufferChan := make(chan int)
有缓存通道
bufferChan := make(chan int, 2) //创建了一个缓存区大小为2的有缓存通道
阻塞机制
-
无缓冲通道
当一个 Goroutine 向无缓存通道发送数据时,该 Goroutine 会被阻塞,直到另一个 Goroutine 从该通道接收数据。
同理,当一个 Goroutine 试图从无缓存通道接收数据时,该 Goroutine 也会被阻塞,直到另一个 Goroutine 向该通道发送数据。 -
有缓存通道
有缓存通道会在内部维护一个缓存区,用于存储发送到通道但还未被接收的数据。
当向有缓存通道发送数据时, 如果缓存区还有空间, 数据会被发送到缓存区中等待被接收, 否则发送者 Gorutine 会被阻塞, 直到有空间可用。
当从一个有缓存通道接收数据时, 如果缓存区有数据, 会立刻接收到该数据, 否则接收者 Groutine 会被阻塞, 直到有数据可接收。
同步与异步
-
无缓冲通道
无缓存通道在发送和接收操作上是"同步"的,也就是说,发送者必须等待接收者接收数据,接收者也必须等待发送者发送数据。 -
有缓存通道
有缓存通道在发送和接收操作上是"异步"的,也就是说,发送者可以先将数据发送到通道缓存区,而不需要等待接收者接收数据。
总的来说,无缓冲通道更加简单和直观,但需要更加小心地管理 Goroutine 的生命周期;而有缓存通道提供了更大的灵活性,但也增加了一定的复杂性。根据具体的需求选择合适的通道类型是非常重要的。