如何 優雅的關閉 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的關閉,對於日常遇到的可以歸結爲三類

1、m個receivers,一個sender.

2、一個receiver,n個sender

3、m個receivers,n個sender

1、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)

	// 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()
}

2、一個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 := 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()
}

3、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 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是否已經關閉。如果你能確定不會向channel發送任何值,那麼也確實需要一個簡單的方法來檢查channel是否已經關閉:

package main

import "fmt"

type T int

func 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。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章