這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
在go中使用linked channels進行資料廣播
原文在這裡(需翻牆),為啥想要翻譯這篇文章是因為在實際中也碰到過如此的問題,而該文章的解決方式很巧妙,希望對大家有用。
在go中channels是一個很強大的東西,但是在處理某些事情上面還是有局限的。其中之一就是一對多的通訊。channels在多個writer,一個reader的模型下面工作的很好,但是卻不能很容易的處理多個reader等待擷取一個writer發送的資料的情況。
處理這樣的情況,可能的一個go api原型如下:
type Broadcaster …func NewBroadcaster() Broadcasterfunc (b Broadcaster) Write(v interface{})func (b Broadcaster) Listen() chan interface{}
broadcast channel通過NewBroadcaster建立,通過Write函數進行資料廣播。為了監聽這個channel的資訊,我們使用Listen,該函數返回一個新的channel去接受Write發送的資料。
這套解決方案需要一個中間process用來處理所有reader的註冊。當調用Listen建立新的channel之後,該channel就被註冊,通常該中間process的主迴圈如下:
for { select { case v := <-inc: for _, c := range(listeners) { c <- v } case c := <- registeryc: listeners.push(c) }}
這是一個通常的做法,(譯者也經常這麼幹)。但是該process在處理資料廣播的時候會阻塞,直到所有的readers讀取到值。一個可選的解決方式就是reader的channel是有buffer緩衝的,緩衝大小我們可以按需調節。或者當buffer滿的時候我們將資料丟棄。
但是這篇blog並不是介紹上面這套做法的。這篇blog主要提出了另一種實現方式用來實現writer永遠不阻塞,一個慢的reader並不會因為writer發送資料太快而要考慮分配太大的buffer。
雖然這麼做不會有太高的效能,但是我並不在意,因為我覺得它很酷。我相信我會找到一個很好的使用地方的。
首先是核心的東西:
type broadcast struct { c chan broadcast v interface{}}
這就是我說的linked channel,但是其實是Ouroboros data structure。也就是,這個struct執行個體在發送到channel的時候包含了自己。
從另一方面來說,如果我有一個chan broadcast類型的資料,那麼我就能從中讀取一個broadcast b,b.v就是writer發送的任意資料,而b.c,這個原先的chan broadcast,則能夠讓我重複這個過程。
另一個可能讓人困惑的地方在於一個帶有緩衝區的channel能夠被用來當做一個1對多廣播的對象。如果我定義如下的buffered channel:
var c = make(chan T, 1)
任何試圖讀取c的process都將阻塞直到有資料寫入。
當我們想廣播一個資料的時候,我們只是簡單的將其寫入這個channel,這個值只會被一個reader給擷取,但是我們約定,只要讀取到了資料,我們立刻將其再次放入該channel,如下:
func wait(c chan T) T { v := <-c c <-v return v}
結合上面兩個討論的東西,我們就能夠發現如果在broadcast struct裡面的channel如果能夠按照上面的方式進行處理,我們就能實現一個資料廣播。
代碼如下:
package broadcasttype broadcast struct { c chan broadcast; v interface{};}type Broadcaster struct { // private fields: Listenc chan chan (chan broadcast); Sendc chan<- interface{};}type Receiver struct { // private fields: C chan broadcast;}// create a new broadcaster object.func NewBroadcaster() Broadcaster { listenc := make(chan (chan (chan broadcast))); sendc := make(chan interface{}); go func() { currc := make(chan broadcast, 1); for { select { case v := <-sendc: if v == nil { currc <- broadcast{}; return; } c := make(chan broadcast, 1); b := broadcast{c: c, v: v}; currc <- b; currc = c; case r := <-listenc: r <- currc } } }(); return Broadcaster{ Listenc: listenc, Sendc: sendc, };}// start listening to the broadcasts.func (b Broadcaster) Listen() Receiver { c := make(chan chan broadcast, 0); b.Listenc <- c; return Receiver{<-c};}// broadcast a value to all listeners.func (b Broadcaster) Write(v interface{}) { b.Sendc <- v }// read a value that has been broadcast,// waiting until one is available if necessary.func (r *Receiver) Read() interface{} { b := <-r.C; v := b.v; r.C <- b; r.C = b.c; return v;}
下面就是譯者的東西了,這套方式實現的很巧妙,首先它解決了reader register以及unregister的問題。其次,我覺得它很好的使用了流式化處理的方式,當reader讀取到了一個值,reader可以將其傳遞給下一個reader繼續使用,同時自己在開始監聽下一個新的值的到來。
譯者自己的一個測試案例:
func TestBroadcast(t *testing.T) { b := NewBroadcaster() r := b.Listen() b.Write("hello") if r.Read().(string) != "hello" { t.Fatal("error string") } r1 := b.Listen() b.Write(123) if r.Read().(int) != 123 { t.Fatal("error int") } if r1.Read().(int) != 123 { t.Fatal("error int") } b.Write(nil) if r.Read() != nil { t.Fatal("error nit") } if r1.Read() != nil { t.Fatal("error nit") }}
當然,這套方式還有點不足,主要就在於Receiver Read函數,並不能很好的與select進行整合,具體可以參考該作者另一篇bloghttp://rogpeppe.wordpress.com/2010/01/04/select-functions-for-go/。