這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。這篇文章中我們會研究一個基本的同步問題。並使用 Golang 中原生的 Buffered Channels 來為這個問題找到一個簡潔的解決方案。## 問題現在假設我們我們有一堆 workers。為了充分發揮 CPU 多核的能力,我們讓每個 worker 運行在單獨的 goroutine 中:```gofor i := 0; i < workers; i++ {go worker()}```worker 需要做一系列的工作 job:```gofunc worker() {for i := 0; i < 3; i++ {job()}}```每次 job 前都需要在所有的 worker 上同步地先進行一次準備 bootstrap 的過程。也就是說,每個 worker 在執行 job 前,需要等待所有其他 worker 都完成 bootstrap 的準備。```gofunc worker() {for i := 0; i < 3; i++ {bootstrap()# wait for other workers to bootstrapjob()}}```還有件事。如果至少有一個 worker 仍在執行 job,則所有 worker 的下一次的 bootstrap 都不能開始。換句話說,每次的 bootstrap 都是為緊接著的 job 部分做準備的,所以不能在上一次的 job 尚未結束之前就開始下一次的 bootstrap:```gofunc worker() {for i := 0; i < 3; i++ {# wait for all workers to finish previous loopbootstrap()# wait for other workers to bootstrapjob()}}```我們的 bootstrap 部分內容為增長一個共用的計數器。job 部分為等待一段時間並列印計數器的內容:```gotype counter struct {c intsync.Mutex}func (c *counter) Incr() {c.Lock()c.c += 1c.Unlock()}func (c *counter) Get() (res int) {c.Lock()res = c.cc.Unlock()return}func worker(c *counter) {for i := 0; i < 3; i++ {# wait for all workers to finish previous loopc.Incr()# wait for other workers to do bootstraptime.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond)fmt.Println(c.Get())}}```我們的目標是編寫一個程式並以下列情形列印數字: * 只有 n, 2\**n* 和 3\**n* 的數字被列印(因為每個 worker 迴圈 3 次) * 每次列印的數字不會比之前的數字小 * 每個數字會被列印 *n* 次如果有 3 個 worker,則期望輸出如下:```333666999```2 個 worker 的期望輸出:```224466```2 個 worker 不合法的輸出可能會是這樣:```242466```想一想可能的解決辦法。下面幾行我故意留空並不急著給大家答案。..............workers 會用一個名為迴環柵欄(reusable barrier,類似 Java 中的 CyclicBarrier)的資料結構來實現同步。每個柵欄包含 2 扇門。第 1 扇門放置於增長計數器之前,一開始是關閉的。關閉的門意味著到達這扇門的 worker 會被阻塞。一旦所有的 worker 到達了第 1 扇門:* 第 2 扇門(放置於增長計數器之後)會關閉* 第 1 扇門開啟所有的計數器會通過並增長計數器,接著成功到達第 2 扇門。一旦所有的 worker 都到達了第 2 扇門:* 第 1 扇門關閉* 第 2 扇門開啟worker 此時可以開始執行 job 並接著在下一次迴圈中再次抵達第 1 扇門。迴圈再次開始。整個過程:``` 1st gate 2nd gatev v-w1--> | | --w2-->|--w3--> | --w4-->| -w5--> | | --w1-->| | --w2-->| --w3-->| --w4-->| --w5-->| || --w1-->| --w2-->|--w3--> | --w4--> || --w5-->|| --w1-->| --w2-->| --w3-->| --w4-->|| --w5-->|--w1--> | || --w2--> --w3-->|--w4--> | | | --w5-->```下文有兩種解決方案,實現略有不同。下面的代碼可用來對兩種方案進行測試:```gopackage mainimport ("fmt""math/rand""sync""time"// Set this import spec to point// to the copy of one of proposed// solutions."path/to/package/barrier")func init() {rand.Seed(time.Now().Unix())}type counter struct {c intsync.Mutex}func (c *counter) Incr() {c.Lock()c.c += 1c.Unlock()}func (c *counter) Get() (res int) {c.Lock()res = c.cc.Unlock()return}func worker(c *counter, br *barrier.Barrier, wg *sync.WaitGroup) {for i := 0; i < 3; i++ {br.Before()c.Incr()br.After()time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond)fmt.Println(c.Get())}wg.Done()}func main() {var wg sync.WaitGroupworkers := 3br := barrier.New(workers)c := counter{}for i := 0; i < workers; i++ {wg.Add(1)go worker(&c, br, &wg)}wg.Wait()}```柵欄必須實現 *Before* 和 *After* 兩個方法,各自對應第 1 和第 2 扇門。## 解決方案 1我們需要容量為 1 的 buffered channel:```goch := make(chan int, 1)```門的邏輯可以通過先從 channel 中接收資料,然後再次向其中發送資料來實現:```go<-chch <- 1```如果 channel 中包含元素數量為 1,則表示門是開的。它會讓一個 worker 通過並往 channel 中放入新的元素以使另外一個 worker 通過,依此類推。如果 channel 中沒有元素了則表示門關閉了。接著 worker 從 channel 中接收元素就會被阻塞。```go// github.com/mlowicki/barrierpackage barrier import "sync"type Barrier struct {c intn intm sync.Mutexbefore chan intafter chan int}func New(n int) *Barrier {b := Barrier{n: n,before: make(chan int, 1),after: make(chan int, 1),}// close 1st gateb.after <- 1 return &b}func (b *Barrier) Before() {b.m.Lock()b.c += 1if b.c == b.n {// close 2nd gate<-b.after// open 1st gateb.before <- 1}b.m.Unlock()<-b.beforeb.before <- 1}func (b *Barrier) After() {b.m.Lock()b.c -= 1if b.c == 0 { // close 1st gate <-b.before // open 2st gate b.after <- 1}b.m.Unlock()<-b.afterb.after <- 1}```## 解決方案 2這個方案使用了容量和 worker 數量 *n* 相等的 buffered channel。現在我們不再讓 worker 一個接一個地依次通過,而是在 channel 中放入 *n* 個元素來使所有的 worker 一次性通過:```go// github.com/mlowicki/barrier2package barrierimport "sync"type Barrier struct {c intn intm sync.Mutexbefore chan intafter chan int}func New(n int) *Barrier {b := Barrier{n: n,before: make(chan int, n),after: make(chan int, n),}return &b}func (b *Barrier) Before() {b.m.Lock()b.c += 1if b.c == b.n {// open 2nd gatefor i := 0; i < b.n; i++ {b.before <- 1}}b.m.Unlock()<-b.before}func (b *Barrier) After() {b.m.Lock()b.c -= 1if b.c == 0 {// open 1st gatefor i := 0; i < b.n; i++ {b.after <- 1}}b.m.Unlock()<-b.after}```## 參考* "The Little Book of Semaphores" --- Allen B. Downey
via: https://medium.com/golangspec/reusable-barriers-in-golang-156db1f75d0b
作者:Michał Łowicki 譯者:alfred-zhong 校對:rxcai
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出
本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽
590 次點擊