理解golang io.Pipe

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

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。可能有點兒繞,其實就是:

  • p.l.Lock()
  • p.rwait.Wait()
  • p.wwait.Wait()
    都是依賴於同一把鎖。這有什麼玄機嗎?——有的!
    如前所述,當writer拿到鎖p.l,然後開始在死迴圈中p.wwait.Wait()等著reader讀完資料時,表面上看起來p.l鎖沒有被釋放,會發生死結。但是,玄機就在p.wwait.Wait上。
    不賣關子了,p.wwait.Wait被調用時,會在內部釋放鎖,而由於p.l和p.wwait.L是同一把鎖,因此reader進去時是可以擷取到鎖的。
    func (c *Cond) Wait() {  c.checker.check()  t := runtime_notifyListAdd(&c.notify)  c.L.Unlock()  runtime_notifyListWait(&c.notify, t)  c.L.Lock()}
    Cond這個東西,要說起來比較複雜,它涉及到runtime,下次會寫一篇文章具體講講。本文主要是講Pipe,所以就不擴充了。

例子

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)    }}
相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.