How To Gracefully Close Channels?(如何優雅地關閉Go channel? )

來源:互聯網
上載者:User

幾天前,我寫了一篇文章來說明golang中channel的使用規範。在reddit和HN,那篇文章收到了很多贊同,但是我也收到了下面幾個關於Go channel設計和規範的批評:

  1. 在不能更改channel狀態的情況下,沒有簡單普遍的方式來檢查channel是否已經關閉了
  2. 關閉已經關閉的channel會導致panic,所以在closer(關閉者)不知道channel是否已經關閉的情況下去關閉channel是很危險的
  3. 發送值到已經關閉的channel會導致panic,所以如果sender(寄件者)在不知道channel是否已經關閉的情況下去向channel發送值是很危險的

那些批評看起來都很有道理(實際上並沒有)。是的,沒有一個內建函數可以檢查一個channel是否已經關閉。如果你能確定不會向channel發送任何值,那麼也確實需要一個簡單的方法來檢查channel是否已經關閉:

package mainimport "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不支援內建SafeSendSafeClose函數,原因就在於並不推薦從接收端或者多個並發發送端關閉channel。Golang甚至禁止關閉只接收(receive-only)的channel。

保持channel closing principle的優雅方案

上面的SafeSend函數有一個缺點是,在select語句的case關鍵字後不能作為發送操作被調用(譯者註:類似於 case SafeSend(ch, t):)。另外一個缺點是,很多人,包括我自己都覺得上面通過使用panic/recoversync包的方案不夠優雅。針對各種情境,下面介紹不用使用panic/recoversync包,純粹是利用channel的解決方案。
(在下面的例子中,sync.WaitGroup只是用來讓例子完整的。它的使用在實踐中不一定一直都有用)

  • M個receivers,一個sender,sender通過關閉data channel說“不再發送”
    這是最簡單的情境了,就只是當sender不想再發送的時候讓sender關閉data 來關閉channel:
package mainimport (    "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)        // the sender    go func() {        for {            if value := rand.Intn(MaxRandomNumber); value == 0 {                // the only sender can close the channel safely.                close(dataCh)                return            } else {                            dataCh <- value            }        }    }()        // receivers    for i := 0; i < NumReceivers; i++ {        go func() {            defer wgReceivers.Done()                        // receive values until dataCh is closed and            // the value buffer queue of dataCh is empty.            for value := range dataCh {                log.Println(value)            }        }()    }        wgReceivers.Wait()}
  • 一個receiver,N個sender,receiver通過關閉一個額外的signal channel說“請停止發送”
    這種情境比上一個要複雜一點。我們不能讓receiver關閉data channel,因為這麼做將會打破channel closing principle。但是我們可以讓receiver關閉一個額外的signal channel來通知sender停止發送值:
package mainimport (    "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 := make(chan struct{})        // stopCh is an additional signal channel.        // Its sender is the receiver of channel dataCh.        // Its reveivers are the senders of channel dataCh.        // 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 {                // the receiver of the dataCh channel is                // also the sender of the stopCh cahnnel.                // It is safe to close the stop channel here.                close(stopCh)                return            }                        log.Println(value)        }    }()        // ...    wgReceivers.Wait()}

正如注釋說的,對於額外的signal channel來說,它的sender是data channel的receiver。這個額外的signal channel被它唯一的sender關閉,遵守了channel closing principle

  • 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 mainimport (    "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 is an additional signal channel.        // Its sender is the moderator goroutine shown below.        // Its reveivers are all senders and receivers of dataCh.    toStop := make(chan string, 1)        // the channel toStop is used to notify the moderator        // to close the additional signal channel (stopCh).        // Its senders are any senders and receivers of dataCh.        // Its reveiver is the moderator goroutine shown below.        var stoppedBy string        // moderator    go func() {        stoppedBy = <- toStop // part of the trick used to notify the moderator                              // to close the additional signal channel.        close(stopCh)    }()        // senders    for i := 0; i < NumSenders; i++ {        go func(id string) {            for {                value := rand.Intn(MaxRandomNumber)                if value == 0 {                    // here, a trick is used to notify the moderator                    // to close the additional signal channel.                    select {                    case toStop <- "sender#" + id:                    default:                    }                    return                }                                // the first select here is to try to exit the                // goroutine as early as possible.                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 {                // same as senders, the first select here is to                 // try to exit the goroutine as early as possible.                select {                case <- stopCh:                    return                default:                }                                select {                case <- stopCh:                    return                case value := <-dataCh:                    if value == MaxRandomNumber-1 {                        // the same trick is used to notify the moderator                         // to close the additional signal 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 toStop的緩衝大小是1.這是為了避免當mederator goroutine 準備好之前第一個通知就已經發送了,導致丟失。

  • 更多的情境?
    很多的情境變體是基於上面三種的。舉個例子,一個基於最複雜情況的變體可能要求receivers讀取buffer channel中剩下所有的值。這應該很容易處理,所有這篇文章也就不提了。
    儘管上面三種情境不能覆蓋所有Go channel的使用情境,但它們是最基礎的,實踐中的大多數情境都可以分類到那三種中。

結論

這裡沒有一種情境要求你去打破channel closing principle。如果你遇到了這種情境,請思考一下你的設計並重寫你的代碼。
用Go編程就像在創作藝術。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.