一、channel使用存在的不方便地方
1、在不改變channel自身狀態的情況下,無法獲知一個channnel是否關閉。
2、關閉一個已經關閉的channel,會導致panic。因此,如果關閉channel的一方在不知道channel是否關閉狀態時就去貿然關閉channel時件很危險的事。
3、向一個已經關閉的channel發送數據會導致panic。因此,如果向channel發送數據的一方不知道channel是否處於關閉狀態就貿然向channel發送數據是很危險的事情。
一個比較粗糙的檢查channel是否關閉的函數:
func IsClosed(ch <-chan interface{}) bool {
select {
case <-ch:
return true
default:
}
return false
}
func main() {
c := make(chan interface{})
fmt.Println(IsClosed(c)) // false
close(c)
fmt.Println(IsClosed(c)) // true
}
上面的代碼其實存在很多問題。
首先,IsClosed函數是一個有副作用的函數。每調用一次,都會讀出channel裏面的一個元素,改變了channel的狀態,這不是一個好函數,幹活就幹活,還順手牽羊?
其次,IsClosed函數返回的結果僅代表調用時候的那個瞬間,並不能保證調用之後不會有其他goroutine對這個channel進行了一些操作,改變了這個channel的狀態。比如:IsClosed函數返回true,但這時有另外一個goroutine關閉了這個channel,這時候我們就會拿着這個過時的"channel未關閉"信息,向其發送數據,就會導致panic的發生。
當然,一個channel不會被重複關閉兩次,如果IsClosed函數返回的結果是true,說明channel是真的關閉了。
有一句廣泛流傳的關閉channel的原則:
不要從一個 receiver 側關閉 channel,也不要在有多個 sender 時,關閉 channel。
向channel發送元素的就是sender,因此sender可以決定何時不發送數據,並且關閉channel。但是如果有多個sender,某個sender同樣沒法確定其他sender的情況。這時候也不能貿然的關閉channel。
有兩個不優雅關閉channel的方法:
1、使用defer-recover機制,放心大膽地關閉channel或者向channel發送數據,即使發生了panic,有defer-recover在兜底。
2、使用sync.Once來保證只關閉一次。
二、如何優雅關閉channel
根據sender和receiver的個數,可以分爲以下幾種情況:
1、一個sender,一個receiver
2、一個sender,M個receiver
3、N個sender,一個receiver
4、N個sender,M個receiver
對於1,2這兩種情況,只有一個sender,直接從sender段關閉就好。
第3種情形下,優雅關閉channel的方法是:唯一的receiver發出一個關閉channel的信號,senders監聽到關閉信號後,停止發送數據。
func main() {
rand.Seed(time.Now().UnixNano())
const Max = 10000
const NumSenders = 1000
dataChan := make(chan int, 100)
stopChan := make(chan struct{})
// senders
for i := 0; i < NumSenders; i++ {
go func() {
for {
select {
// 監聽關閉channel的信號,退出
case <-stopChan:
return
// 給dataChan中發送數據
case dataChan <- rand.Intn(Max):
}
}
}()
}
// receiver
go func() {
// 從dataChan中遍歷數據
for value := range dataChan {
// 如果value的值爲9999,通知sender停止發送數據
if value == Max-1 {
fmt.Println("send stop signal to senders.")
close(stopChan)
return
}
fmt.Println(value)
}
}()
select {
// 阻塞一小時退出
case <-time.After(time.Hour):
}
}
這裏的stopCh就是信號channel,它本身只有一個sender,因此可以直接關閉它。senders收到了關閉信號後,select分支case<-stopCh
被選中,退出函數,不再發送數據。
需要說明的是,上面的代碼並沒有明確關閉dataCh。在Go語言中,對於一個channel,如果最終沒有任何goroutine引用它,不管channel有沒有被關閉,最終都會被GC回收。所以在這種情形下,所謂的優雅地關閉channel就是不關閉channel,讓GC代勞。
最後一種情況,優雅關閉channel的方法是:any one of them says “let’s end the game” by notifying a moderator to close an additional signal channel。
和第三種情況不同,這裏有M個receiver,如果,採用第3種解決方案,由receiver直接關閉stopCh的話,就會重複關閉一個channel,導致panic。因此需要增加一箇中間人,M個receiver都向它發送關閉dataCh的"請求",中間人收到第一個請求後,就會直接下達關閉dataCh的指令(通過關閉stopCh,這時就不會發生重複關閉的情況,因爲stopCh的發送方只有中間人一個)。另外這裏N個sender也可以向中間人發送關閉dataCh的請求。
func main() {
rand.Seed(time.Now().UnixNano())
const Max = 10000
const NumReceivers = 10
const NumSenders = 1000
dataCh := make(chan int, 100)
stopCh := make(chan struct{})
// 這裏必須是一個有緩衝的channel
toStop := make(chan string, 1)
var stoppedBy string
// 中間人
go func() {
// 接收到了關閉channel的請求
stoppedBy = <-toStop
// 發送關閉dataCh的信號
close(stopCh)
}()
// senders
for i := 0; i < NumSenders; i++ {
go func(id string) {
for {
value := rand.Intn(Max)
// 如果value=0,則給中間人發送關閉channel的信號
if value == 0 {
select {
case toStop <- "sender#" + id:
// 此處是爲了防止toStop這個channel阻塞
default:
}
return
}
select {
// 監聽關閉channel的信號
case <-stopCh:
return
case dataCh <- value:
}
}
}(strconv.Itoa(i))
}
// receivers
for i := 0; i < NumReceivers; i++ {
go func(id string) {
for {
select {
// 監聽關閉信號,退出,停止接收數據
case <-stopCh:
return
// 接收數據
case value := <-dataCh:
// 如果接收到了9999,則給中間人發送關閉channel的信號
if value == Max-1 {
select {
case toStop <- "receiver#" + id:
default:
}
return
}
fmt.Println(value)
}
}
}(strconv.Itoa(i))
}
select {
case <-time.After(time.Hour):
}
}
代碼裏toStop就是中間人的角色,使用它來接收senders和receivers發送過來的關閉dataCh請求。
這裏將toStop聲明成了一個緩衝型的channel。假設toStop聲明的是一個非緩衝型的channel,那麼第一個發送的關閉dataCh請求可能會丟失。因爲無論是sender還是receiver都是通過select語句來發送請求,如果中間人所在的goroutine沒有準備好,那麼select語句就不會被選中,直接走default選項了,什麼都不做。這樣,第一個關閉dataCh的請求就會丟失。
如果把toStop的容量聲明成Num(senders) + Num(receivers),那麼發送dataCh請求的部分可以改寫成更簡潔的形式:
func main() {
rand.Seed(time.Now().UnixNano())
const Max = 10000
const NumReceivers = 10
const NumSenders = 1000
dataCh := make(chan int, 100)
stopCh := make(chan struct{})
// 這裏必須是一個有緩衝的channel
toStop := make(chan string, NumSenders+NumReceivers)
var stoppedBy string
// 中間人
go func() {
// 接收到了關閉channel的請求
stoppedBy = <-toStop
// 發送關閉dataCh的信號
close(stopCh)
}()
// senders
for i := 0; i < NumSenders; i++ {
go func(id string) {
for {
value := rand.Intn(Max)
// 如果value=0,則給中間人發送關閉channel的信號
if value == 0 {
toStop <- "sender#" + id
return
}
select {
// 監聽關閉channel的信號
case <-stopCh:
return
case dataCh <- value:
}
}
}(strconv.Itoa(i))
}
// receivers
for i := 0; i < NumReceivers; i++ {
go func(id string) {
for {
select {
// 監聽關閉信號,退出,停止接收數據
case <-stopCh:
return
// 接收數據
case value := <-dataCh:
// 如果接收到了9999,則給中間人發送關閉channel的信號
if value == Max-1 {
toStop <- "receiver#" + id
return
}
fmt.Println(value)
}
}
}(strconv.Itoa(i))
}
select {
case <-time.After(time.Hour):
}
}
直接向toStop發送請求,因爲toStop容量足夠大,所以不用擔心阻塞,自然也就不用select語句再加一個default case來避免阻塞。
可以看到,這裏同樣沒有真正關閉dataCh,原樣通第3種情況。
參考鏈接: