在不能更改channel状态的情况下,没有简单普遍的方式来检查channel是否已经关闭了。
关闭已经关闭的channel会导致panic,所以在closer(关闭者)不知道channel是否已经关闭的情况下去关闭channel是很危险的。
发送值到已经关闭的channel会导致panic,所以如果sender(发送者)在不知道channel是否已经关闭的情况下去向channel发送值是很危险的。
那么如何优雅的关闭 channel 呢?
在使用Go channel的时候,一个适用的原则是不要从接收端关闭channel,也不要在多个并发发送端中关闭channel。换句话说,如果sender(发送者)只是唯一的sender或者是channel最后一个活跃的sender,那么你应该在sender的goroutine关闭channel,从而通知receiver(s)(接收者们)已经没有值可以读了。维持这条原则将保证永远不会发生向一个已经关闭的channel发送值或者关闭一个已经关闭的channel。(我们将会称上面的原则为channel closing principle)
保持channel closing principle的优雅方案
channel closing principle要求我们只能在发送端进行channel的关闭,对于日常遇到的可以归结为三类:
m个receivers,一个sender.
m个receivers,一个sender.
m个receivers,n个sender
01 m个receivers,一个sender
M个receivers,一个sender,sender通过关闭data channel说“不再发送”。这是最简单的场景了,就只是当sender不想再发送的时候让sender关闭data 来关闭channel:
package main
import (
"time"
"math/rand"
"sync"
"log"
)
func main() {
rand.Seed(time.Now().UnixNano())
log.SetFlags(0)
const MaxRandomNumber = 100000
const NumReceivers = 100
wgReceivers := sync.WaitGroup{}
wgReceivers.Add(NumReceivers)
dataCh := make(chan int, 100)
// sender
go func() {
for {
if value := rand.Intn(MaxRandomNumber); value == 0 {
// 唯一的 sender 可以安全的关闭 channel.
close(dataCh)
return
} else {
dataCh <- value
}
}
}()
// receivers
for i := 0; i < NumReceivers; i++ {
go func() {
defer wgReceivers.Done()
// 会一直等待并接收消息,直到 dataCh 关闭 缓冲队列 dataCh 为空
for value := range dataCh {
log.Println(value)
}
}()
}
wgReceivers.Wait()
}02一个receiver,n个senders
一个receiver,N个sender,receiver通过关闭一个额外的signal channel说 “请停止发送” 这种场景比上一个要复杂一点。我们不能让receiver关闭data channel,因为这么做将会打破channel closing principle。但是我们可以让receiver关闭一个额外的signal channel来通知sender停止发送值:
package main
import (
"time"
"math/rand"
"sync"
"log"
)
func main() {
rand.Seed(time.Now().UnixNano())
log.SetFlags(0)
const MaxRandomNumber = 100000
const NumSenders = 1000
wgReceivers := sync.WaitGroup{}
wgReceivers.Add(1)
dataCh := make(chan int, 100)
// stopCh 是一个附加的信号 channel.
stopCh := make(chan struct{})
// senders
for i := 0; i < NumSenders; i++ {
go func() {
for {
value := rand.Intn(MaxRandomNumber)
select {
case <- stopCh:
return
case dataCh <- value:
}
}
}()
}
// the receiver
go func() {
defer wgReceivers.Done()
for value := range dataCh {
if value == MaxRandomNumber-1 {
// 接收者 dataCh channel 同时也是 stopCh channel 的发送者
// 这里安全的关闭了 stopCh channel
close(stopCh)
return
}
log.Println(value)
}
}()
// 一直等待同步结束
wgReceivers.Wait()
}03 m个receivers,n个sender
M个receiver,N个sender,它们当中任意一个通过通知一 moderator(仲裁者)关闭额外的signal channel来说 “让我们结束游戏吧” 这是最复杂的场景了。我们不能让任意的receivers和senders关闭data channel,也不能让任何一个receivers通过关闭一个额外的signal channel来通知所有的senders和receivers退出游戏。这么做的话会打破channel closing principle。但是,我们可以引入一个moderator来关闭一个额外的signal channel。这个例子的一个技巧是怎么通知moderator去关闭额外的signal channel:
package main
import (
"time"
"math/rand"
"sync"
"log"
"strconv"
)
func main() {
rand.Seed(time.Now().UnixNano())
log.SetFlags(0)
const MaxRandomNumber = 100000
const NumReceivers = 10
const NumSenders = 1000
wgReceivers := sync.WaitGroup{}
wgReceivers.Add(NumReceivers)
dataCh := make(chan int, 100)
stopCh := make(chan struct{})
// stopCh 是一个额外的信号 channel.
// 它的 sender 是下面的 moderator goroutine.
// Its reveivers are all senders and receivers of dataCh.
toStop := make(chan string, 1)
// channel toStop 用来通知 moderator
// 来关闭额外的信号 channel (stopCh).
// 它的 senders 是这里任何的 senders 和 dataCh 的 receivers.
// 它的 reveiver 是下面的 moderator goroutine.
var stoppedBy string
// moderator
go func() {
stoppedBy = <- toStop
// 用来通知 moderator 来关闭额外的信号 channel.
close(stopCh)
}()
// senders
for i := 0; i < NumSenders; i++ {
go func(id string) {
for {
value := rand.Intn(MaxRandomNumber)
if value == 0 {
// 这里一个 select 用来通知 moderator
// 来关闭额外的信号 channel.
select {
case toStop <- "sender#" + id:
default:
}
return
}
// 这里的 select 用来尽量早的退出 当前 goroutine
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 {
// 和 senders 一样, 这里的第一个 select 用来
// 尽可能早的退出当前 goroutine.
select {
case <- stopCh:
return
default:
}
select {
case <- stopCh:
return
case value := <-dataCh:
if value == MaxRandomNumber-1 {
// 同样的手法来通知 moderator
// 来关闭额外的信号 channel.
select {
case toStop <- "receiver#" + id:
default:
}
return
}
log.Println(value)
}
}
}(strconv.Itoa(i))
}
wgReceivers.Wait()
log.Println("stopped by", stoppedBy)
}打破channel closing principle
有没有一个内置函数可以检查一个channel是否已经关闭。如果你能确定不会向channel发送任何值,那么也确实需要一个简单的方法来检查channel是否已经关闭:
package main
import "fmt"
type T intfunc IsClosed(ch <-chan T) bool {
select {
case <-ch:
return true
default:
}
return false
}
func main() {
c := make(chan T)
fmt.Println(IsClosed(c))
// false
close(c)
fmt.Println(IsClosed(c))
// true
}上面已经提到了,没有一种适用的方式来检查channel是否已经关闭了。但是,就算有一个简单的 closed(chan T) bool函数来检查channel是否已经关闭,它的用处还是很有限的,就像内置的len函数用来检查缓冲channel中元素数量一样。原因就在于,已经检查过的channel的状态有可能在调用了类似的方法返回之后就修改了,因此返回来的值已经不能够反映刚才检查的channel的当前状态了。
尽管在调用closed(ch)返回true的情况下停止向channel发送值是可以的,但是如果调用closed(ch)返回false,那么关闭channel或者继续向channel发送值就不安全了(会panic)。
The Channel Closing Principle
在使用Go channel的时候,一个适用的原则是不要从接收端关闭channel,也不要在多个并发发送端中关闭channel。换句话说,如果sender(发送者)只是唯一的sender或者是channel最后一个活跃的sender,那么你应该 sender的goroutine关闭channel,从而通知receiver(s)(接收者们)已经没有值可以读了。维持这条原则将保证永远不会发生向一个已经关闭的channel发送值或者关闭一个已经关闭的channel。(下面,我们将会称上面的原则为channel closing principle)
打破channel closing principle的解决方案
如果你因为某种原因从接收端(receiver side)关闭channel或者在多个发送者中的一个关闭channel,那么你应该使用列在Golang panic/recover Use Cases的函数来安全地发送值到channel中(假设channel的元素类型是T)
func SafeSend(ch chan T, value T) (closed bool) {
defer func() {
if recover() != nil {
// the return result can be altered
// in a defer function call
closed = true
}
}()
ch <- value
// panic if ch is closed
return false
// <=> closed = false; return
}如果channel ch没有被关闭的话,那么这个函数的性能将和ch <- value接近。对于channel关闭的时候,SafeSend函数只会在每个sender goroutine中调用一次,因此程序不会有太大的性能损失。同样的想法也可以用在从多个goroutine关闭channel中:
func SafeClose(ch chan T) (justClosed bool) {
defer func() {
if recover() != nil {
justClosed = false
}
}()
// assume ch != nil here.
close(ch)
// panic if ch is closed
return true
}很多人喜欢用sync.Once来关闭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) })
}当然了,我们也可以用sync.Mutex来避免多次关闭channel:
type MyChannel struct {
C chan T
closed bool
mutex sync.Mutex
}
func NewMyChannel() *MyChannel {
return &MyChannel{C: make(chan T)}
}
func (mc *MyChannel) SafeClose() {
mc.mutex.Lock()
if !mc.closed {
close(mc.C)
mc.closed = true
}
mc.mutex.Unlock()
}
func (mc *MyChannel) IsClosed() bool {
mc.mutex.Lock()
defer mc.mutex.Unlock()
return mc.closed
}我们应该要理解为什么Go不支持内置SafeSend和SafeClose函数,原因就在于并不推荐从接收端或者多个并发发送端关闭channel。Golang甚至禁止关闭只接收(receive-only)的channel。
感谢阅读!
喜欢本文的朋友,欢迎关注“isevena”