這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
golang的io包中,稍微有點兒晦澀的就是Pipe方法,今天我們就一起來看一看這個Pipe。
函數定義如下:
func Pipe() (*PipeReader, *PipeWriter)
它返回了一個Reader和一個Writer
起初一看是有點兒奇怪的,很少有這麼用的哦,它到底能幹嘛呢?
其實它返回的不僅僅是簡單的一個Writer一個Reader,它返回的是息息相關的一個Writer和一個Reader。
下面我先用比較口語化的方式來講一下它們是如何工作的。
假設
先假設我們在工地上,有兩個工人,一個叫w,一個叫r,w負責搬磚,而r負責砌牆。
初始
w和r一起配合工作,一開始啥都沒有,負責砌牆的r就沒法工作,於是它開始睡覺(Wait
)。而w只能去搬磚了。
磚來了
w深知r懶惰的習性,當它把磚搬過來後,就把r叫醒(Signal
)。然後w心想,反正你砌牆也要一會兒,那我也睡會兒。於是w叫醒r後它也開始睡覺(Wait
)。
砌牆
r被叫醒之後,心想著睡了這麼久害怕被包工頭責罵,自然就開始辛勤地砌牆了,很快就把w搬過來的磚用完了。r心想,這牆砌不完可怪不到我頭上,因為沒磚了,於是r叫醒了w,然後自己又去睡覺了。
繼續搬磚
w被叫醒後一看,哎喲我去,這麼快就沒磚了?然後他又跑去搬了些轉過來,然後叫醒睡得跟死豬一樣的r起來砌牆,自己又開始睡覺……
周而復始,直到……
w和r兩人就這麼周而復始地配合,直到r發現牆砌好了,或者w發現工地上已經沒有磚了。
以上大概就是Pipe的通俗的解釋。不過問題也來了,這倆人瞌睡怎麼這麼多呢?w幹活r就歇著,不能同時幹嗎?答案是——不能
為什嗎?因為Pipe就是為了某些特定情境而提出的。看看官方文檔的說明:
Reads and Writes on the pipe are matched one to one except when multiple Reads are needed to consume a single Write
也就是說,Pipe適用於,產生了一條資料,緊接著就要處理掉這條資料的情境。而且因為其內部是一把大鎖,因此是安全執行緒的。
內部實現
來看看內部實現,先看看read
func (p *pipe) read(b []byte) (n int, err error) { // One reader at a time. p.rl.Lock() defer p.rl.Unlock() p.l.Lock() defer p.l.Unlock() for { if p.rerr != nil { return 0, ErrClosedPipe } if p.data != nil { break } if p.werr != nil { return 0, p.werr } p.rwait.Wait() } n = copy(b, p.data) p.data = p.data[n:] if len(p.data) == 0 { p.data = nil p.wwait.Signal() } return}
這段代碼,我用虛擬碼簡化一下:
func (p *pipe) read(b []byte) (n int, err error) { 各種加鎖() for { if 有資料可以讀或者哪裡有錯 { break } 讓出時間片等待被喚醒,如果是被正常調度回來的依然不醒,必須是被指名點姓叫醒才醒() } copy(b, p.data) 通知writer可以繼續寫資料進來了()}
write其實也是大同小異:
func (p *pipe) write(b []byte) (n int, err error) { 各種加鎖() p.data = b 通知reader有資料了() for { if 資料被讀完了或者哪裡有錯 { break } 讓出時間片等待被喚醒,如果是被正常調度回來的依然不醒,必須是被指名點姓叫醒才醒() } p.data = nil}
看了虛擬碼,再看看實際代碼,應該就很容易了。但是還有幾個地方需要細說,第一個就是鎖的問題。
在read中:
func (p *pipe) read(b []byte) (n int, err error) { // One reader at a time. p.rl.Lock() defer p.rl.Unlock() p.l.Lock() defer p.l.Unlock() // ...
而在write中:
func (p *pipe) write(b []byte) (n int, err error) { // pipe uses nil to mean not available if b == nil { b = zero[:] } // One writer at a time. p.wl.Lock() defer p.wl.Unlock() p.l.Lock() defer p.l.Unlock() if p.werr != nil { err = ErrClosedPipe return } // ...
可能你注意到了,read和write都會去取同一把鎖p.l
。
假設我們writer和reader在兩個不同的goroutine中執行,並且write先執行,那麼依照上面的代碼,write會先拿鎖,當執行完
p.data = b
之後會通知reader,然後自己進入一個死迴圈裡進行Wait,直到reader把p.data讀完。但是問題來了,writer進入死迴圈時並沒有釋放鎖p.l
,然後reader一直等待p.l釋放然後去讀取資料,而writer一直在等reader讀取完資料才能跳出去釋放鎖。看起來這是一個死結?
我只能說“Naive”,官方標準庫怎麼會犯這麼低級的錯誤呢?但是代碼就這樣,該如何解釋?
其實,關鍵在於那個sync.Cond
type pipe struct { rl sync.Mutex // gates readers one at a time wl sync.Mutex // gates writers one at a time l sync.Mutex // protects remaining fields data []byte // data remaining in pending write rwait sync.Cond // waiting reader wwait sync.Cond // waiting writer rerr error // if reader closed, error to give writes werr error // if writer closed, error to give reads}
rwait和wwait都是sync.Cond
,這是什麼東東呢?
看下它的文檔:
// Cond implements a condition variable, a rendezvous point// for goroutines waiting for or announcing the occurrence// of an event.//// Each Cond has an associated Locker L (often a *Mutex or *RWMutex),// which must be held when changing the condition and// when calling the Wait method.//// A Cond can be created as part of other structures.// A Cond must not be copied after first use.type Cond struct { noCopy noCopy // L is held while observing or changing the condition L Locker notify notifyList checker copyChecker}
Cond如果要細說的話,又得寫另一篇文章了。在這裡你只要知道sync.Cond其內部依賴於一個Locker。
而且在初始化時:
func Pipe() (*PipeReader, *PipeWriter) { p := new(pipe) p.rwait.L = &p.l p.wwait.L = &p.l r := &PipeReader{p} w := &PipeWriter{p} return r, w}
可以看到rwait和wwait都是依賴於用一把鎖,而且這把鎖就是p.l。可能有點兒繞,其實就是:
例子
Pipe的使用情境,我覺得極少數情境可能才會需要用到,我目前沒有想到非常需要Pipe的情境。因為每次Read需要等Write寫完,是串列的情境。不過Pipe的好處是,由於它把Write的slice放到p.data中,這是一次引用賦值。之後Read時,把p.data copy出去,本質上相當於copy了write的未經處理資料,並沒有用臨時slice儲存,減少了記憶體使用量量。
我感覺也就那麼回事兒吧,為此你不得不再開個goroutine,gotoutine雖然輕量級,但也不是沒有開銷,當然它的開銷和分配記憶體比就小巫見大巫了。我個人感覺,如果你的應用沒有對記憶體要求嚴苛到這種層級,Pipe也沒什麼卵用。
如果你發現了Pipe比較合適的情境,非常希望告訴我!
下面給出一個強行使用Pipe的代碼:起了多個goroutine作為writer,每個writer內部隨機產生字串寫進去。唯一的reader讀取資料並列印:
var r = rand.New(rand.NewSource(time.Now().UnixNano()))func generate(writer *PipeWriter) { arr := make([]byte, 32) for { for i := 0; i < 32; i++ { arr[i] = byte(r.Uint32() >> 24) } n, err := writer.Write(arr) if nil != err { log.Fatal(err) } time.Sleep(200 * time.Millisecond) }}func main() { rp, wp := Pipe() for i := 0; i < 20; i++ { go generate(wp) } time.Sleep(1 * time.Second) data := make([]byte, 64) for { n, err := rp.Read(data) if nil != err { log.Fatal(err) } if 0 != n { log.Println("main loop", n, string(data)) } time.Sleep(1 * time.Second) }}