[翻譯]Go併發模式:構建和終止流水線

Go併發模式:構建和終止流水線 (Go Concurrency Patterns: Pipelines and cancellation)

原著:Sameer Ajmani 2014-03-12

翻譯:Narcism 2020-04-02

文中代碼邏輯分析
文中示例代碼的示意圖

介紹

​ Go的併發特性(concurrency primitives)讓它輕易的構建可以有效利用I / O和多個CPU的流數據流水線 (streaming data pipeline)。這篇文章介紹了一些這種流水線的例子,重點介紹了操作失敗時出現的細微差別,並介紹了完整的處理錯誤的技術。

什麼是流水線(pipeline)

​ 流水線(pipeline)在go中並沒有官方的定義,它只是多種併發模式中的一種。非官方定義,流水線是由通道(channel)連接起來的一系列的階段,每個階段是一組相同功能的goroutine.在每個階段中,這些goroutines:

  • 從上游(upstream)通過輸入通道(inbound channels)接受數據
  • 對數據進行一些處理,通常會產生新的數據
  • 把數據通道輸出通道(outbound channels)發送到下游(downstream)

​ 除了只有輸出通道的第一個階段和只有輸入通道的最後一個階段外,每一個階段由任意個輸入通道和輸出通道。通常把第一個階段叫做 source 或生產者(producer),把最後一個階段叫sink或消費者(consumer)。

​ 在文章中首先會通過一個簡單的例子來解釋流水線(pipeline)的創意和技巧。然後會展示一個更接近實際應用的的例子。

計算平方數

​ 這個流水線由三個階段組成。

​ 第一個階段是gin,它是一個把數字組成的列表轉換到一個發出整個列表中數字的通道(channel)的方法。它會打開一個goroutine,這個goroutine會發送數字到通道,並在數字都發送完之後關閉該通道:

func gen(nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        for _, n := range nums {
            out <- n
        }
        close(out)
    }()
    return out
}

​ 第二個階段是sq,它從一個輸入通道(inbound channel)接收數字,並返回一個發送接受到的數字的平方的輸出通道(outbound channels)。當輸入通道(inbound channels)關閉並且這個階段把所有的值都發送到下游(downstream)後,它就會關閉輸出通道(outbound channels):

func sq(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        for n := range in {
            out <- n * n
        }
        close(out)
    }()
    return out
}

​ 第三個階段在主方法main中,mian方法主要聲明流水線,並運行最後一個階段:它從第二個階段中接收數據並挨個打印(print)出來,直到第二階段中的輸出通道關閉。

func main() {
    // Set up the pipeline.
    c := gen(2, 3)
    out := sq(c)

    // Consume the output.
    fmt.Println(<-out) // 4
    fmt.Println(<-out) // 9
}

​ 因爲sq方法的輸入和輸出的通道是同一種類型,所以可以多次使用它進行整個流水線的組合。我們也可以把main改成一種像其他的階段一樣的循環的方式進行print

func main() {
    // Set up the pipeline and consume the output.
    for n := range sq(sq(gen(2, 3))) {
        fmt.Println(n) // 16 then 81
    }
}

扇出,扇入(fan-out,Fan-in)

​ 扇出(fan-out)多個方法可以從同一個尚未關閉的通道(channel)讀數據。這提供了一種並行使用CPU和I/O的方法。(This provides a way to distribute work amongst a group of workers to parallelize CPU use and I/O)。

​ 扇入(fan-in)是一個方法能夠從多個輸入通道中讀取數據,並一直讀取直到所有的通道都關閉,通過將多個輸入通道多路複用到一個(當所有的輸入通道關閉的時候關閉的)通道。

​ A function can read from multiple inputs and proceed until all are closed by multiplexing the input channels onto a single channel that’s closed when all the inputs are closed. This is called fan-in.

​ 我們把流水線編程運行兩個sq實例,每一個都從相同的輸入通道讀取數據。然後用一個新的方法merge扇入sq的的輸出:

func main() {
    in := gen(2, 3)

    // Distribute the sq work across two goroutines that both read from in.
    c1 := sq(in)
    c2 := sq(in)

    // Consume the merged output from c1 and c2.
    for n := range merge(c1, c2) {
        fmt.Println(n) // 4 then 9, or 9 then 4
    }
}

merge方法通過爲每一個輸入通道打開一個把輸入通道的數據發送到輸出通道的goroutine來將多個通道的數據轉換到一個通道上。所有的這些goroutine啓動後,merge再打開一個goroutine在上 面這些goroutine結束後關閉輸出通道。

​ 爲了避免把數據推到已經關閉的通道上而引起panic,等所有的goroutine結束後再關閉輸出通道就變得很重要。 sync.WaitGroup 提供了簡單的方法解決這個問題:

func merge(cs ...<-chan int) <-chan int {
    var wg sync.WaitGroup
    out := make(chan int)

    // Start an output goroutine for each input channel in cs.  output
    // copies values from c to out until c is closed, then calls wg.Done.
    output := func(c <-chan int) {
        for n := range c {
            out <- n
        }
        wg.Done()
    }
    wg.Add(len(cs))
    for _, c := range cs {
        go output(c)
    }

    // Start a goroutine to close out once all the output goroutines are
    // done.  This must start after the wg.Add call.
    go func() {
        wg.Wait()
        close(out)
    }()
    return out
}

突然停止(stopping short)

​ 我們的流水線(pipeline)業務有這樣一種模式:

  • 當發送數據的操作取消後,各個階段會關閉他們的輸出通道。
  • 各個階段會不斷的從輸入通道獲取數據,直到輸入通道被關閉。

​ 這種模式讓每一個階段看起開都是一個循環,並確保一旦所有的值都成功發送,所有的goroutine都會關閉。

​ 但是在真實的流水線系統中,有些階段並不是能夠接收到所有的輸入數據。有時候我們會把程序設計成只需要接受到一部分數據就可以確保運行。更多的時候,因爲輸入數據表示上游的階段發生了錯誤而導致本階段結束。在這兩種情況下,接受方不需要一直等待來接受數據,並且,我們希望上游的階段在下游不在需要數據的時候就不在產生新的數據。

​ 在示例的流水心啊中,如果一個階段失敗導致沒有消費所有的輸入數據,嘗試發送這些數據的goroutine就會一直阻塞:

    // Consume the first value from the output.
    out := merge(c1, c2)
    fmt.Println(<-out) // 4 or 9
    return
    // Since we didn't receive the second value from out,
    // one of the output goroutines is hung attempting to send it.
}

​ 這是資源泄漏:goroutines消耗內存和運行時資源,goroutine堆棧中的堆引用會阻止數據被垃圾回收。 goroutine不會被垃圾回收;他們必須自己退出。

​ 即使下游階段沒有收到上游傳來的所有數據,我們也需要安排上游階段退出。第一種方法:可以給輸出通道添加一個緩衝區,緩衝區可以保留特定數量的數據;如果緩衝區內還有空間,那麼發送操作瞬間就完成了:

c := make(chan int, 2) // buffer size 2
c <- 1  // succeeds immediately
c <- 2  // succeeds immediately
c <- 3  // blocks until another goroutine does <-c and receives 1

​ 如果在創建通道的時候就知道了要發送的數據量,就可以簡單的編寫聲明帶緩衝區的通道的代碼。例如,我們可以重寫gen函數使其可以複製所有的數字數組到一個帶緩衝區的通道,而且沒有創建一個新的goroutine:

func gen(nums ...int) <-chan int {
    out := make(chan int, len(nums))
    for _, n := range nums {
        out <- n
    }
    close(out)
    return out
}

​ 考慮到我們流水線中阻塞的goroutine,我們可以考慮在merge中的輸出通道中加入一個緩衝區:

func merge(cs ...<-chan int) <-chan int {
    var wg sync.WaitGroup
    out := make(chan int, 1) // enough space for the unread inputs
    // ... the rest is unchanged ...

​ 儘管這種方式修復了阻塞的goroutine,這依舊是錯誤的代碼。我們在這裏選擇1個緩衝區,是因爲我們知道上游會發送的數據量和下游會消費的數據量。一旦上游多發送一個數據,或者下游少消費一個數據,這個程序依舊會阻塞。

​ 所以我們需要爲下游的階段提供一種通知發送數據階段停止發送數據的方法。

明確退出(explicit cancellation)

​ 當main函數決定在沒有接受所有數據的時候退出,它必須通知所有的上游階段的goroutine放棄他們正在嘗試發送的值。它可以通過給done通道發送值。因爲可能有兩個阻塞的發送者,所以要給done發送兩個值:

func main() {
    in := gen(2, 3)

    // Distribute the sq work across two goroutines that both read from in.
    c1 := sq(in)
    c2 := sq(in)

    // Consume the first value from output.
    done := make(chan struct{}, 2)
    out := merge(done, c1, c2)
    fmt.Println(<-out) // 4 or 9

    // Tell the remaining senders we're leaving.
    done <- struct{}{}
    done <- struct{}{}
}

​ 發送數據的goroutine要把他們發送數據的操作替換成用select的方式。select操作要麼發送數據到out或者從done通道接受數據。因爲done中的數據無關緊要,所以發送的是空的struct:它只是一個接收事件來通知放棄發送數據到out。這時output的goroutine會繼續循環從輸入通道c中讀取數據,而不會造成阻塞。(稍後我們會討論如何讓循環提早退出)。

func merge(done <-chan struct{}, cs ...<-chan int) <-chan int {
    var wg sync.WaitGroup
    out := make(chan int)

    // Start an output goroutine for each input channel in cs.  output
    // copies values from c to out until c is closed or it receives a value
    // from done, then output calls wg.Done.
    output := func(c <-chan int) {
        for n := range c {
            select {
            case out <- n:
            case <-done:
            }
        }
        wg.Done()
    }
    // ... the rest is unchanged ...

​ 這種方式進行退出有一個問題:每一個下游都需要知道上游會有多少個可能阻塞的發送者,讓這些發送者提早結束。持續的記錄追蹤這些值不僅無聊而且容易出錯。

​ 我們需要一種方法來來通知我們不清出數量或者一直在發送數據的goroutine不再發送數據到下游。在GO中,我們可以通過關閉channel來實現這種操作。因爲在關閉通道上的接受操作總是會立馬成功,儘管接受的值是空值。

​ 這意味着main可以通過關閉done通道來防止發送者可能造成的阻塞。關閉操作時間上是對所有的發送者進行廣播。我們在每流水線的每一個方法中都把done作爲一個接收參數。然後通過defer關閉輸出通道。這樣就可以通過main控制所有的goroutine進行退出,防止阻塞。

func main() {
    // Set up a done channel that's shared by the whole pipeline,
    // and close that channel when this pipeline exits, as a signal
    // for all the goroutines we started to exit.
    done := make(chan struct{})
    defer close(done)

    in := gen(done, 2, 3)

    // Distribute the sq work across two goroutines that both read from in.
    c1 := sq(done, in)
    c2 := sq(done, in)

    // Consume the first value from output.
    out := merge(done, c1, c2)
    fmt.Println(<-out) // 4 or 9

    // done will be closed by the deferred call.
}

​ 這樣流水線上的各個階段在done關閉後直接結束。因爲sq中的輸出通道在done關閉後不在發送數據,所以merge中的outputroutine會在不耗盡輸入通道的情況下結束。output通過defer確保wg.Done執行:

func merge(done <-chan struct{}, cs ...<-chan int) <-chan int {
    var wg sync.WaitGroup
    out := make(chan int)

    // Start an output goroutine for each input channel in cs.  output
    // copies values from c to out until c or done is closed, then calls
    // wg.Done.
    output := func(c <-chan int) {
        defer wg.Done()
        for n := range c {
            select {
            case out <- n:
            case <-done:
                return
            }
        }
    }
    // ... the rest is unchanged ...

​ 同樣的sq可以在done關閉後直接返回。sq也通過defer確保輸出通道關閉。

func sq(done <-chan struct{}, in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for n := range in {
            select {
            case out <- n * n:
            case <-done:
                return
            }
        }
    }()
    return out
}

​ 流水線構建指南:

  • 當發送數據的操作取消後,各個階段會關閉他們的輸出通道。
  • 各個階段會不斷的從輸入通道獲取數據,直到輸入通道被關閉。

    流水線通過確保__有足夠的緩存區__或通過__接受放棄發送數據的信號__來確保協程不會阻塞。

消化一顆樹(digesting a tree)

​ 下面是一個更接近實際應用的流水線。

​ MD5是一種用於文件校驗的消息摘要(message-digest)算法。命令行程序md5sum是用來打印文件列表的摘要值:

% md5sum *.go
d47c2bbc28298ca9befdfbc5d3aa4e65  bounded.go
ee869afd31f83cbb2d10ee81b2b831dc  parallel.go
b88175e65fdcbc01ac08aaf1fd9b5e96  serial.go

​ 我們的示例程序與MD5相似,但是我們接受的是一個文件夾,計算文件夾內每個文件的摘要值,並按照路徑名稱進行排序打印。

% go run serial.go .
d47c2bbc28298ca9befdfbc5d3aa4e65  bounded.go
ee869afd31f83cbb2d10ee81b2b831dc  parallel.go
b88175e65fdcbc01ac08aaf1fd9b5e96  serial.go

​ 主函數調用了一個MD5All函數(這個函數會返回一個路徑:摘要值的map),然後對結果進行排序打印。

func main() {
    // Calculate the MD5 sum of all files under the specified directory,
    // then print the results sorted by path name.
    m, err := MD5All(os.Args[1])
    if err != nil {
        fmt.Println(err)
        return
    }
    var paths []string
    for path := range m {
        paths = append(paths, path)
    }
    sort.Strings(paths)
    for _, path := range paths {
        fmt.Printf("%x  %s\n", m[path], path)
    }
}

MD5All是我們討論的重點。在serial.go中,只是遍歷了文件夾,並沒後用到併發。

// MD5All reads all the files in the file tree rooted at root and returns a map
// from file path to the MD5 sum of the file's contents.  If the directory walk
// fails or any read operation fails, MD5All returns an error.
func MD5All(root string) (map[string][md5.Size]byte, error) {
    m := make(map[string][md5.Size]byte)
    err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
        if err != nil {
            return err
        }
        if !info.Mode().IsRegular() {
            return nil
        }
        data, err := ioutil.ReadFile(path)
        if err != nil {
            return err
        }
        m[path] = md5.Sum(data)
        return nil
    })
    if err != nil {
        return nil, err
    }
    return m, nil
}

並行消化(parallel digestion)

parallel.go中,把MD5All分成了兩個階段的流水線,第一個階段是sumFiles,遍歷文件夾,每個文件都在一個新的goroutine中計算摘要值,然後把計算的結果通過result類型發送到通道:

type result struct {
    path string
    sum  [md5.Size]byte
    err  error
}

sumFiles返回兩個通道:一個是result類型的通道另一個是filepath.Walk返回的error類型的通道。在walk方法中,每一個文件都會用一個新的goroutine,然後檢查done的狀態。如果done關閉了,walk就會立刻停止:

func sumFiles(done <-chan struct{}, root string) (<-chan result, <-chan error) {
    // For each regular file, start a goroutine that sums the file and sends
    // the result on c.  Send the result of the walk on errc.
    c := make(chan result)
    errc := make(chan error, 1)
    go func() {
        var wg sync.WaitGroup
        err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
            if err != nil {
                return err
            }
            if !info.Mode().IsRegular() {
                return nil
            }
            wg.Add(1)
            go func() {
                data, err := ioutil.ReadFile(path)
                select {
                case c <- result{path, md5.Sum(data), err}:
                case <-done:
                }
                wg.Done()
            }()
            // Abort the walk if done is closed.
            select {
            case <-done:
                return errors.New("walk canceled")
            default:
                return nil
            }
        })
        // Walk has returned, so all calls to wg.Add are done.  Start a
        // goroutine to close c once all the sends are done.
        go func() {
            wg.Wait()
            close(c)
        }()
        // No select needed here, since errc is buffered.
        errc <- err
    }()
    return c, errc
}

MD5All接收來自c通道的摘要值。MD5All如果發生錯誤會提前返回,所以通過defer確保done通道一定被關閉。

func MD5All(root string) (map[string][md5.Size]byte, error) {
    // MD5All closes the done channel when it returns; it may do so before
    // receiving all the values from c and errc.
    done := make(chan struct{})
    defer close(done)

    c, errc := sumFiles(done, root)

    m := make(map[string][md5.Size]byte)
    for r := range c {
        if r.err != nil {
            return nil, r.err
        }
        m[r.path] = r.sum
    }
    if err := <-errc; err != nil {
        return nil, err
    }
    return m, nil
}

有界並行(Bounded parallelism)

​ 在parallel.go中的MD5All給每一個文件都開啓了一個新的goroutine。如果文件夾內有很多大文件,可能會造成分配的資源多餘計算機上可用的資源。

我們可以通過限定並行運行的文件個數來限制這種資源分配。在bounded.go 中,通過創建特定數量的goroutines達到這種限制。現在我們的流水線分成三個階段:遍歷樹,讀取並計算文件的摘要值,收集摘要值。

​ 第一個階段,walkFiles,輸出文件夾中文件的地址:

func walkFiles(done <-chan struct{}, root string) (<-chan string, <-chan error) {
    paths := make(chan string)
    errc := make(chan error, 1)
    go func() {
        // Close the paths channel after Walk returns.
        defer close(paths)
        // No select needed for this send, since errc is buffered.
        errc <- filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
            if err != nil {
                return err
            }
            if !info.Mode().IsRegular() {
                return nil
            }
            select {
            case paths <- path:
            case <-done:
                return errors.New("walk canceled")
            }
            return nil
        })
    }()
    return paths, errc
}

​ 第二個階段開啓固定數量的goroutine,從地址(path)中獲取文件名計算摘要值並把結果發送給c通道:

func digester(done <-chan struct{}, paths <-chan string, c chan<- result) {
    for path := range paths {
        data, err := ioutil.ReadFile(path)
        select {
        case c <- result{path, md5.Sum(data), err}:
        case <-done:
            return
        }
    }
}

​ 跟之前的例子不通,因爲有多個goroutine在同一個通道上發送值,digester並沒有關閉輸出通道c。__digester可以對標上面例子中的output這個函數對象,在output中也沒有關閉通道。__關閉這個通道是在MD5All中進行的。(每個函數只會關閉自己的輸出通道,而對其他的通道不會進行關閉操作,也就是,通道在哪個方法中被定義的,就會在哪個方法中被關閉。這樣可以避免混亂):

    // Start a fixed number of goroutines to read and digest files.
    c := make(chan result)
    var wg sync.WaitGroup
    const numDigesters = 20
    wg.Add(numDigesters)
    for i := 0; i < numDigesters; i++ {
        go func() {
            digester(done, paths, c)
            wg.Done()
        }()
    }
    go func() {
        wg.Wait()
        close(c)
    }()

​ 也可以對每個digester都創建一個自己的輸出通道,但是這樣的話就需要一個額外的goroutine進行扇入(fan-in)各個digester的結果。(對標計算平方的方法,MD5Allsqmerge兩個方程整合到了一起,把計算過程整合到了MD5All)。

​ 最後一個階段從c中接受所有的result,並檢查從errc中接受的error。檢查操作只能在這裏執行,因爲如果再早的話,就可能造成walkFiles的阻塞:

    m := make(map[string][md5.Size]byte)
    for r := range c {
        if r.err != nil {
            return nil, r.err
        }
        m[r.path] = r.sum
    }
    // Check whether the Walk failed.
    if err := <-errc; err != nil {
        return nil, err
    }
    return m, nil
}

結論

​ 本篇文章介紹了用GO構建流數據流水線的技術。因爲每個階段都有可能在嘗試向下遊發送數據的時候造成阻塞,同時下游也可能不在關心新進來的數據,這導致處理這種流水線的程序的錯誤會很棘手。文中也展示瞭如何通過關閉done來廣播發送停止所有goroutine的方法,同時定義了正確構建流水線的指南。
更多閱讀:

serial.go

// +build OMIT

package main

import (
	"crypto/md5"
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"sort"
)

// MD5All reads all the files in the file tree rooted at root and returns a map
// from file path to the MD5 sum of the file's contents.  If the directory walk
// fails or any read operation fails, MD5All returns an error.
func MD5All(root string) (map[string][md5.Size]byte, error) {
	m := make(map[string][md5.Size]byte)
	err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { // HL
		if err != nil {
			return err
		}
		if !info.Mode().IsRegular() {
			return nil
		}
		data, err := ioutil.ReadFile(path) // HL
		if err != nil {
			return err
		}
		m[path] = md5.Sum(data) // HL
		return nil
	})
	if err != nil {
		return nil, err
	}
	return m, nil
}

func main() {
	// Calculate the MD5 sum of all files under the specified directory,
	// then print the results sorted by path name.
	m, err := MD5All(os.Args[1]) // HL
	if err != nil {
		fmt.Println(err)
		return
	}
	var paths []string
	for path := range m {
		paths = append(paths, path)
	}
	sort.Strings(paths) // HL
	for _, path := range paths {
		fmt.Printf("%x  %s\n", m[path], path)
	}
}

parallel.go

// +build OMIT

package main

import (
	"crypto/md5"
	"errors"
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"sort"
	"sync"
)

// A result is the product of reading and summing a file using MD5.
type result struct {
	path string
	sum  [md5.Size]byte
	err  error
}

// sumFiles starts goroutines to walk the directory tree at root and digest each
// regular file.  These goroutines send the results of the digests on the result
// channel and send the result of the walk on the error channel.  If done is
// closed, sumFiles abandons its work.
func sumFiles(done <-chan struct{}, root string) (<-chan result, <-chan error) {
	// For each regular file, start a goroutine that sums the file and sends
	// the result on c.  Send the result of the walk on errc.
	c := make(chan result)
	errc := make(chan error, 1)
	go func() { // HL
		var wg sync.WaitGroup
		err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
			if err != nil {
				return err
			}
			if !info.Mode().IsRegular() {
				return nil
			}
			wg.Add(1)
			go func() { // HL
				data, err := ioutil.ReadFile(path)
				select {
				case c <- result{path, md5.Sum(data), err}: // HL
				case <-done: // HL
				}
				wg.Done()
			}()
			// Abort the walk if done is closed.
			select {
			case <-done: // HL
				return errors.New("walk canceled")
			default:
				return nil
			}
		})
		// Walk has returned, so all calls to wg.Add are done.  Start a
		// goroutine to close c once all the sends are done.
		go func() { // HL
			wg.Wait()
			close(c) // HL
		}()
		// No select needed here, since errc is buffered.
		errc <- err // HL
	}()
	return c, errc
}

// MD5All reads all the files in the file tree rooted at root and returns a map
// from file path to the MD5 sum of the file's contents.  If the directory walk
// fails or any read operation fails, MD5All returns an error.  In that case,
// MD5All does not wait for inflight read operations to complete.
func MD5All(root string) (map[string][md5.Size]byte, error) {
	// MD5All closes the done channel when it returns; it may do so before
	// receiving all the values from c and errc.
	done := make(chan struct{}) // HLdone
	defer close(done)           // HLdone

	c, errc := sumFiles(done, root) // HLdone

	m := make(map[string][md5.Size]byte)
	for r := range c { // HLrange
		if r.err != nil {
			return nil, r.err
		}
		m[r.path] = r.sum
	}
	if err := <-errc; err != nil {
		return nil, err
	}
	return m, nil
}

func main() {
	// Calculate the MD5 sum of all files under the specified directory,
	// then print the results sorted by path name.
	m, err := MD5All(os.Args[1])
	if err != nil {
		fmt.Println(err)
		return
	}
	var paths []string
	for path := range m {
		paths = append(paths, path)
	}
	sort.Strings(paths)
	for _, path := range paths {
		fmt.Printf("%x  %s\n", m[path], path)
	}
}

bounded.go

// +build OMIT

package main

import (
	"crypto/md5"
	"errors"
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"sort"
	"sync"
)

// walkFiles starts a goroutine to walk the directory tree at root and send the
// path of each regular file on the string channel.  It sends the result of the
// walk on the error channel.  If done is closed, walkFiles abandons its work.
func walkFiles(done <-chan struct{}, root string) (<-chan string, <-chan error) {
	paths := make(chan string)
	errc := make(chan error, 1)
	go func() { // HL
		// Close the paths channel after Walk returns.
		defer close(paths) // HL
		// No select needed for this send, since errc is buffered.
		errc <- filepath.Walk(root, func(path string, info os.FileInfo, err error) error { // HL
			if err != nil {
				return err
			}
			if !info.Mode().IsRegular() {
				return nil
			}
			select {
			case paths <- path: // HL
			case <-done: // HL
				return errors.New("walk canceled")
			}
			return nil
		})
	}()
	return paths, errc
}

// A result is the product of reading and summing a file using MD5.
type result struct {
	path string
	sum  [md5.Size]byte
	err  error
}

// digester reads path names from paths and sends digests of the corresponding
// files on c until either paths or done is closed.
func digester(done <-chan struct{}, paths <-chan string, c chan<- result) {
	for path := range paths { // HLpaths
		data, err := ioutil.ReadFile(path)
		select {
		case c <- result{path, md5.Sum(data), err}:
		case <-done:
			return
		}
	}
}

// MD5All reads all the files in the file tree rooted at root and returns a map
// from file path to the MD5 sum of the file's contents.  If the directory walk
// fails or any read operation fails, MD5All returns an error.  In that case,
// MD5All does not wait for inflight read operations to complete.
func MD5All(root string) (map[string][md5.Size]byte, error) {
	// MD5All closes the done channel when it returns; it may do so before
	// receiving all the values from c and errc.
	done := make(chan struct{})
	defer close(done)

	paths, errc := walkFiles(done, root)

	// Start a fixed number of goroutines to read and digest files.
	c := make(chan result) // HLc
	var wg sync.WaitGroup
	const numDigesters = 20
	wg.Add(numDigesters)
	for i := 0; i < numDigesters; i++ {
		go func() {
			digester(done, paths, c) // HLc
			wg.Done()
		}()
	}
	go func() {
		wg.Wait()
		close(c) // HLc
	}()
	// End of pipeline. OMIT

	m := make(map[string][md5.Size]byte)
	for r := range c {
		if r.err != nil {
			return nil, r.err
		}
		m[r.path] = r.sum
	}
	// Check whether the Walk failed.
	if err := <-errc; err != nil { // HLerrc
		return nil, err
	}
	return m, nil
}

func main() {
	// Calculate the MD5 sum of all files under the specified directory,
	// then print the results sorted by path name.
	m, err := MD5All(os.Args[1])
	if err != nil {
		fmt.Println(err)
		return
	}
	var paths []string
	for path := range m {
		paths = append(paths, path)
	}
	sort.Strings(paths)
	for _, path := range paths {
		fmt.Printf("%x  %s\n", m[path], path)
	}

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