Go語言中非同步拆分io.Reader

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

原文地址

在Go語言中處理任何stream資料時,我已經深陷io.Readerio.Writer的靈活性中不能自拔。同時我在有一點上又或多或少的受了些折磨,挑戰我的reader interface在你看來可能會覺得很簡單:那就是怎麼樣拆分讀操作。

我甚至不知道使用“拆分(split)”這個詞是否正確,我就是想通過io.Reader多次讀取接收到的東西,有時候可能還需要並行操作。但是由於readers不一定會暴露Seek方法重設讀取位置,我需要一個方法來複製它。或者可以算是clone或fork嗎?

現狀

假設你有一個web服務允許使用者上傳一個檔案。這個服務將會把檔案儲存體在雲端。但是在儲存前需要對這個檔案進行一些簡單的處理。對於接下來的所有請求,你都不得不使用io.Reader去處理。

解決方案

當然,有不止一種方法可以處理這種情況。根據檔案的類型,服務的輸送量,以及檔案需要的處理方式的不同有些方式可能比其他的更合適。下面,我給出了5中不同複雜度和靈活性的方法。可以想象還會有更多的方法,但是這幾個會是一個不錯的起點。

Solution #1:簡單的bytes.Reader

如果源reader沒有Seek方法,為什麼不自己實現一個呢?你可以把所有的內容都讀取到一個bytes.Reader中,然後你想分多少次讀取都可以,只要你開心:

func handleUpload(u io.Reader)(err error) {    //capture all bytes from upload    b, err := ioutil.ReadAll(u)    if err != nil {        return err    }    //wrap the bytes in a ReaderSeeker    r := bytes.NewReader(b)    //process the metadata    err = processMetaData(r)    if err != nil {        return err    }    r.Seek(0, 0)    //upload the data    err = uploadFile(r)    if err != nil {        return err    }    return nil}

如果資料足夠小,這可能是最方便的選擇;你可以完全忘掉bytes.Reader並使用*byte slice的方式代替工作。但是假如是大檔案,如視頻檔案或RAW格式的照片等。這些龐然大物將吞噬你的記憶體,特別是如果服務還具有高流量特徵時。更何況(not to mention)你不能並存執行這些操作。

  • 優點:最簡單的方案
  • 缺點:同步,無法適應你期望的很多、很大的檔案。

Solution #2:可靠的檔案系統

OK,那麼將資料放到磁碟中的檔案如何(藉助ioutil.TempFile),並且可以避免將資料存放區在記憶體中帶來的隱患。

func handleUpload(u io.Reader)(err error) {    //create a temporary file for the upload    f, err := ioutil.TempFile("", "upload")    if err != nil {        return err    }    //destroy the file once done    defer func() {        n := f.name()        f.Close()        os.Remove(n)    }()    //transfer the bytes to the file    _, err := io.Copy(f, u)    if err != nil {        return err    }    //rewind the file    f.Seek(0.0)    //upload the file    err = uploadFile(f)    if err != nil{        return err    }    return nil}

如果最終是要將檔案儲存體在service啟動並執行檔案系統中,這種方法可能是最好的選擇(儘管會產生一個真實的臨時檔案),但是我們假設它最終將落在雲上。繼續,如果這個檔案同樣很大,則將產生顯著的,但是不必要的IO。同時,你還將面臨機器上單個檔案錯誤或宕機的風險,所以如果你的資料比較敏感,我也不推薦這種方式。

  • 優點:避免大量記憶體佔用儲存整個檔案
  • 缺點:同步,潛在的佔用大量IO、磁碟空間以及資料單點故障

Solution #3:The Duct-Tape io.MultiReader

有些情況下,你需要的metadata存在於檔案最開始的幾個位元組。例如,識別一個JPEG格式的檔案只需要檢查檔案的前兩個位元組是否是0xFF 0xD8。這個可以通過使用io.MultiReader同步處理。io.MultiReader將一組readers組織起來使他們看起來像一個一樣。如下是我們的JPEG樣本:

func handleUpload(u io.Reader)(err error) {    //read in the first 2 bytes    b := make([]byte, 2)    _, err := u.Read(b)    if err != nil {        return err    }    //check that they match the JPEG header    jpg := []byte{0xFF, 0xD8}    if !bytes.Equal(b, jpg) {        return errors.New("not a JPEG")    }    //glue those bytes back onto the reader    r := io.MultiReader(bytes.NewReader(b), u)    //upload the file     err = uploadFile(r)    if err != nil {        return err    }    return nil}

如果你只打算上傳JPEG檔案,這是一個很好的技術。只需要兩個位元組,你就可以停止傳輸(註:此處的傳輸不是檔案上傳的傳輸,而是將檔案拷貝到記憶體或磁碟進行處理的傳輸過程),而不必將整個檔案拷貝到記憶體或存放到磁碟上。你應該也會發現,有些情境這個方法也並不適用。比如你需要讀取更多的檔案內容去收集資料,如通過計算統計單詞個數等。這個過程會阻塞檔案上傳,對任務密集型可能也不是理想的處理方式。最後,大多數第三方包(和大部分標準庫)將完整的消耗掉一個reader,以防止你以這種方式使用io.MultiReader.

另一種方案是使用bufio.Reader.Peek。本質上它執行相同的操作,但是你可以避開MultiReader。也就是說,它還可以讓你訪問Reader上的其他的有用的方法。

  • 優點:快速且是對檔案頭的髒讀,可以作為檔案上傳的門檻。
  • 缺點:不適用於不定長讀取,處理整個檔案,密集任務,或和很多第三方包一同使用。

Solution #4:The Single-Split io.TeeReader and io.Pipe

回到前面討論的大視頻檔案的情況,我們稍微修改一下故事情節。你的使用者只會上傳單一格式的視頻檔案,但是你希望這些視頻檔案能夠被你的服務以不同格式播放。比如說,你有一個第三方轉碼器可以將io.Reader讀取的MP4格式資料轉換成WebM格式的資料輸出。你的服務將會把原始的MP4和轉碼的WebM檔案都上傳到雲端。前面的方案必須同步的執行這些操作,現在你想要並行的完成這件事情。

看看io.TeeReader,它的函數簽名是這樣的:func TeeReader(r Reader, w Writer) Reader。文檔中是這樣描述的:TeeReader將從Reader r讀取的資料返回一個寫到Writer w的Reader。這個正是你所需要的!現在你怎麼確保寫到w的資料可讀?這個是通過io.Pipe實現的,它在io.PipeWriterio.PipeReader之間建立了一個串連(即棧,後入先出)。看看代碼是怎麼實現的:

func HandleUpload(u io.Reader) (err error) {    //create the pipe and tee reader    pr, pw := io.Pipe()    tr := io.TeeReader(u, pw)    //Create channels to synchronize    done := make(chan bool)    errs := make(chan error)    defer close(done)    defer close(errs)    go func() {        //close the PipeWriter after the         //TeeReader completes to trigger EOF        defer pw.Close()        //upload the original MP4 data        err := uploadFile(tr)        if err != nil {            errs <- err            return        }        done <- true    }()    go func() {        //transcode to WebM        webmr, err := transcode(pr)        if err != nil {            errs <- err            return        }        //upload to storage        err = uploadFile(webmr)        if err != nil {            errs <- err            return        }        done <- true    }()    //wait until both are done    //or an error occurs    for c := 0; c < 2; {        select {        case err := <-errs:            return err        case <- done:            c++        }    }    return nil}

因為uploader將要消費tr,transcoder在將資料存放區前接收並處理相同的資料。所有的操作不需要額外的buffer,並且並行的執行。注意這裡使用goroutine來執行這兩天路徑。io.Pipe處於阻塞狀態直到有程式向它寫或從它讀取資料。如果嘗試在同一個線程中執行相同的io.Pipe,將會得到一個致命錯誤:fatal error;all goroutines are asleep - deadlockpanic。另一個需要注意的點是:使用Pipe時,你需要在一個合適的時間顯示的觸發一個EOF來關閉io.PipeWriter。在這個實力中,需要在TeeReader結束後關閉它。

這個樣本同樣採用了channel來進行goroutines之間的“doneness”和error的同步。如果你期望在執行過程中有一些更具體的值返回,你可以使用更合適的類型替換chan bool。

  • 優點:完全獨立的,並行的處理相同的資料流
  • 缺點,使用goroutines和channel增加了複雜度

Solution #5:The Multi-Split io.MultiWriter and io.Copy

io.TeeReader在只有一個其他的流消費者時,能夠非常好的解決問題。由於service可能還需要並行的處理更多的任務(如,轉換成更多的格式),使用tee的疊加將使代碼變得臃腫。看看io.MultiWriter的解釋:“一個將writes複製並提供給多個writers的writer”。它也像前面的方法一樣使用pipes來傳播資料,不同的是,不是使用io.TeeReader,而是使用io.Copy將資料分發到所有的Pipes。範例程式碼如下:

func handleUpload(u io.Reader)(err error) {    //create the pipes    mp4R, mp4W := io.Pipe()    webmR, webmW := io.Pipe()    oggR, oggW := io.Pipe()    wavR, wavW := io.Pipe()    //create channels to syschronize    done := make(chan bool)    errs := make(chan error)    defer close(done)    defer close(err)    //spawn all the task goroutines. these looks identical to    //the TeeReader example, but pulled out into separate     //methods for clarity    go uploadMP4(mp4R, done, errs)    go transcodeAndUploadWebM(webmR, done, errs)    go transcodeAndUploadOgg(webmR, done, errs)    go transcodeAndUploadWav(webmR, done, errs)    go func() {        // after completing the copy, we need to close        // the PipeWriters to propagate the EOF to all         // PipeReaders to avoid deadlock        defer mp4W.Close()        defer webmW.Close()        defer oggW.Close()        defer wavW.Close()        //build the multiwriter for all the pipes        mw := io.MultiWriter(mp4W, webmW, oggW, wavW)        //copy the data into the multiwriter        _, err := io.Copy(mw, u)        if err != nil {            errs <- err        }    }()     // wait until all are done     // or an error occurs     for c := 0; c < 4; c++ {        select {        case err := <-errs:            return err        case <-done:        }    }    return nil}

這個方法和前面的方法有點類似,但是當資料需要被複製多次時,這種方法明顯的更加簡潔。因為使用了PIPEs,同樣需要使用goroutines和同步channel,以防止死結。我們在copy完成了關閉了所有的pipes。

  • 優點:可以根據需要fork多份未經處理資料
  • 缺點:過多的依賴goroutines和channel進行協調。

關於channels?

Channels是Go提供的獨特的,強大的並發工具之一。它是goroutines之間的橋樑,同時兼顧了通訊和同步。你可以建立帶buffer和不帶buffer的channel,來實現資料共用。那麼,為什麼我不提供一個充分利用Channels的解決方案,而不僅僅是用作同步呢?

查閱了一些標準庫的top-level包,發現channels很少出現在函數簽名中:

  • time: 用於select timeout
  • reflect: …cause reflection
  • fmt: for formatting it as a pointer
  • builtin: expose the close function

io.Pipe的實現中放棄了channel,而使用sync.Mutex來安全的在reader和writer之間移動資料。我懷疑這是因為Channel的效能並不好,所以在這裡才被Mutex替代。

當開發一個可重複利用的包的時候,我會像標準庫一樣在我公開的API中避免使用Channels,但是會在內部使用它們用作同步。如果複雜度足夠的低,使用mutex替代channel也許更加理想。這也就是說,在程式開發中,channel是更完美的抽象,比lock更好使用,更加靈活。

拋磚迎玉

我在這裡只是拋出了屈指可數的幾種方法處理從io.Reader擷取的資料,毫無疑問,肯定還有更多的方法。Go的隱式介面模型(implicit interface model)+ 標準庫的大量使用允許創造性的將不同組件組合而不用擔心資料。我希望我在這裡的一些探討對你有所協助,正如它們對我有用一樣.

聯繫我們

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