這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。本文作者提供了在 2016 的 GopherCon 上的關於 Go 並發可視化的[主題演講視頻](https://www.youtube.com/watch?v=KyuFeiG3Y60)。Go 語言一個鮮明的優點就是內建的基於 [CSP](https://en.wikipedia.org/wiki/Communicating_sequential_processes) 的並發實現。Go 可以說是一個為了並發而設計的語言,允許我們使用它構建複雜的並發流水線。但是開發人員是否在腦海中想象過不同的併發模式呢,它們在你的大腦中是怎樣的形狀?你肯定想過這些!我們都會靠多種多樣的想象來思考。如果我讓你想象一下 1-100 的數字,你會下意識地在腦海中閃過這些數位映像。比如說我會把它想象成一條從我出發的直線,到 20 時它會右轉 90 度並且一直延伸到 1000。這是因為我上幼兒園的時候,衛生間牆上寫滿了數字,20 正好在角落上。你們腦中肯定有自己想象的數字形象。另一個常見的例子就是一年四季的可視化,有些人把它想象成一個盒子,另外一些人把它想象成是圓圈。無論如何,我想要用 Go 和 WebGL 分享我想象的一些常用併發模式的可視化。這些可視化或多或少地代表了我頭腦中的並發編程方法。如果能知道我們之間對並發可視化想象的差異,肯定是一件很有趣的事情。我尤其想要知道 Rob Pike 和 Sameer Ajmani 是怎麼想象並發的,那一定很有意思。現在,我們就從最簡單的 “Hello, Concurrent World” 開始,來瞭解我腦海中的並發世界吧。## Hello, Concurrent World這個例子的代碼很簡單,只包含一個 channel,一個 goroutine,一個讀操作和一個寫操作。```gopackage mainfunc main() { // create new channel of type int ch := make(chan int) // start new anonymous goroutine go func() { // send 42 to channel ch <- 42 }() // read from channel <-ch}```[WebGL 動畫介面](http://divan.github.io/demos/hello)![hello](https://raw.githubusercontent.com/studygolang/gctt-images/master/visualizing-concurrency/hello.gif)在這張圖中,藍色的線代表 goroutine 的時間軸。串連 `main` 和 `go#19` 的藍線是用來標記 goroutine 的起始和終止並且表示父子關係的。紅色的箭頭代表的是 send/recv 操作。儘管 send/recv 操作是兩個獨立的操作,但是我試著將它們表示成一個操作 `從 A 發送到 B`。右邊藍線上的 `#19` 是該 goroutine 的內部 ID,可以通過 Scott Mansfield 在 [Goroutine IDs](http://blog.sgmansfield.com/2015/12/goroutine-ids/) 一文中提到的技巧擷取。## 計時器(Timers)事實上,我們可以通過簡單的幾個步驟編寫一個計時器:建立一個 channel,啟動一個 goroutine 以給定間隔往 channel 中寫資料,將這個 chennel 返回給調用者。調用者阻塞地從 channel 中讀,就會得到一個精準的時鐘。讓我們來試試調用這個程式 24 次並且將過程可視化。```gopackage mainimport "time"func timer(d time.Duration) <-chan int { c := make(chan int) go func() { time.Sleep(d) c <- 1 }() return c}func main() { for i := 0; i < 24; i++ { c := timer(1 * time.Second) <-c }}```[WebGL 動畫介面](http://divan.github.io/demos/timers/)![timers](https://raw.githubusercontent.com/studygolang/gctt-images/master/visualizing-concurrency/timers.gif)這個效果是不是很有條理?## 乒乓球(Ping-pong)這個例子是我從 Google 員工 Sameer Ajmani 的一次演講 ["Advanced Go Concurrency Patterns"](https://talks.golang.org/2013/advconc.slide#1) 中找到的。當然,這並不是一個很高階的並行存取模型,但是對於 Go 語言並發的新手來說是很有趣的。在這個例子中,我們定義了一個 channel 來作為“乒乓桌”。乒乓球是一個整形變數,代碼中有兩個 goroutine “玩家”通過增加乒乓球的 counter 在“打球”。```gopackage mainimport "time"func main() { var Ball int table := make(chan int) go player(table) go player(table) table <- Ball time.Sleep(1 * time.Second) <-table}func player(table chan int) { for { ball := <-table ball++ time.Sleep(100 * time.Millisecond) table <- ball }}```[WebGL 動畫介面](http://divan.github.io/demos/pingpong/)![Ping-pong](https://raw.githubusercontent.com/studygolang/gctt-images/master/visualizing-concurrency/pingpong.gif)我建議你點擊 “WebGL 動畫介面” 連結,從不同角度看看這個模型,並且試試它減速,加速的效果。現在,我們給這個模型添加一個玩家(goroutine)。```go go player(table) go player(table) go player(table)```[WebGL 動畫介面](http://divan.github.io/demos/pingpong3/)![Ping-pong2](https://raw.githubusercontent.com/studygolang/gctt-images/master/visualizing-concurrency/pingpong3.gif)我們可以看到每個 goroutine 都有序地“打到球”,你可能會好奇這個行為的原因。那麼,為什麼這三個 goroutine 始終按照一定順序接收到 ball 呢?答案很簡單,Go 運行時會對每個 channel 的所有接收者維護一個 [FIFO 隊列 ](https://github.com/golang/go/blob/master/src/runtime/chan.go#L34)。在我們的例子中,每個 goroutine 會在它將 ball 傳給 channel 之後就開始等待 channel,所以它們在隊列裡的順序總是一定的。讓我們增加 goroutine 的數量,看看順序是否仍然保持一致。```gofor i := 0; i < 100; i++ { go player(table)}```[WebGL 動畫介面](http://divan.github.io/demos/pingpong100/)![Ping-pong100](https://raw.githubusercontent.com/studygolang/gctt-images/master/visualizing-concurrency/pingpong100.gif)很明顯,它們的順序仍然是一定的。我們可以建立一百萬個 goroutine 去嘗試,但是上面的實驗已經足夠讓我們得出結論了。接下來,讓我們來看看一些不一樣的東西,比如說通用的訊息模型。## 扇入模式(Fan-In)扇入(fan-in)模式在並發世界中廣泛使用。扇出(fan-out)模式與其相反,我們會在下面介紹。簡單來說,扇入模式就是一個函數從多個輸入源讀取資料並且複用到單個 channel 中。比如說:```gopackage mainimport ( "fmt" "time")func producer(ch chan int, d time.Duration) { var i int for { ch <- i i++ time.Sleep(d) }}func reader(out chan int) { for x := range out { fmt.Println(x) }}func main() { ch := make(chan int) out := make(chan int) go producer(ch, 100*time.Millisecond) go producer(ch, 250*time.Millisecond) go reader(out) for i := range ch { out <- i }}```[WebGL 動畫介面](http://divan.github.io/demos/fanin/)![Fan-In](https://raw.githubusercontent.com/studygolang/gctt-images/master/visualizing-concurrency/fanin.gif)我們能看到,第一個 `producer` 每隔一百毫秒產生一個值,第二個 `producer` 每隔 250 毫秒產生一個值,但是 `reader` 會立即接收它們的值。main 函數中的 for 迴圈高效地接收了 channel 發送的所有資訊。## 工作者模式(Workers)與扇入模式相反的模式叫做扇出(fan-out)或者工作者(workers)模式。多個 goroutine 可以從相同的 channel 中讀資料,利用多核並發完成自身的工作,這就是工作者(workers)模式的由來。在 Go 中,這個模式很容易實現,只需要啟動多個以 channel 作為參數的 goroutine,主函數傳資料給這個 channel,資料分發和複用會由 Go 運行環境自動完成。```gopackage mainimport ( "fmt" "sync" "time")func worker(tasksCh <-chan int, wg *sync.WaitGroup) { defer wg.Done() for { task, ok := <-tasksCh if !ok { return } d := time.Duration(task) * time.Millisecond time.Sleep(d) fmt.Println("processing task", task) }}func pool(wg *sync.WaitGroup, workers, tasks int) { tasksCh := make(chan int) for i := 0; i < workers; i++ { go worker(tasksCh, wg) } for i := 0; i < tasks; i++ { tasksCh <- i } close(tasksCh)}func main() { var wg sync.WaitGroup wg.Add(36) go pool(&wg, 36, 50) wg.Wait()}```[WebGL 動畫介面](http://divan.github.io/demos/workers/)![Workers](https://raw.githubusercontent.com/studygolang/gctt-images/master/visualizing-concurrency/workers.gif)在這裡需要提一下並行結構(parallelism)。我們可以看到,動圖中所有的 goroutine 都是平行“延伸”,等待 channel 給它們發資料來啟動並執行。我們還可以注意到兩個 goroutine 接收資料之間幾乎是沒有停頓的。不幸的是,這個動畫並沒有用顏色區分一個 goroutine 是在等資料還是在執行工作,這個動畫是在 `GOMAXPROCS=4` 的情況下錄製的,所以只有 4 個 goroutine 能夠同時運行。我們將會在下文匯總討論這個主題。現在,我們來寫更複雜一點的代碼,啟動帶有子工作者的工作者(subworkers):```gopackage mainimport ( "fmt" "sync" "time")const ( WORKERS = 5 SUBWORKERS = 3 TASKS = 20 SUBTASKS = 10)func subworker(subtasks chan int) { for { task, ok := <-subtasks if !ok { return } time.Sleep(time.Duration(task) * time.Millisecond) fmt.Println(task) }}func worker(tasks <-chan int, wg *sync.WaitGroup) { defer wg.Done() for { task, ok := <-tasks if !ok { return } subtasks := make(chan int) for i := 0; i < SUBWORKERS; i++ { go subworker(subtasks) } for i := 0; i < SUBTASKS; i++ { task1 := task * i subtasks <- task1 } close(subtasks) }}func main() { var wg sync.WaitGroup wg.Add(WORKERS) tasks := make(chan int) for i := 0; i < WORKERS; i++ { go worker(tasks, &wg) } for i := 0; i < TASKS; i++ { tasks <- i } close(tasks) wg.Wait()}```[WebGL 動畫介面](http://divan.github.io/demos/workers2/)![Workers](https://raw.githubusercontent.com/studygolang/gctt-images/master/visualizing-concurrency/workers2.gif)當然,我們可以將工作者的數量或子工作者的數量設得更高,但是在這裡我們試著不讓動畫效果變得太複雜。Go 中還存在比這更酷的扇出模式,比如動態工作者/子工作者數量,使用 channel 來傳輸 channel,但是現在的動畫類比應該已經可以解釋扇出模型的含義了。## 伺服器(Servers)下一個要說的常用模式和扇出相似,但是它會在短時間內產生多個 goroutine 來完成某些任務。這個模式常被用來實現伺服器 -- 建立一個監聽器,在迴圈中運行 accept() 並針對每個接受的串連啟動 goroutine 來完成指定任務。這個模式很形象並且它能儘可能地簡化伺服器 handler 的實現。讓我們來看一個簡單的例子:```gopackage mainimport "net"func handler(c net.Conn) { c.Write([]byte("ok")) c.Close()}func main() { l, err := net.Listen("tcp", ":5000") if err != nil { panic(err) } for { c, err := l.Accept() if err != nil { continue } go handler(c) }}```[WebGL 動畫介面](http://divan.github.io/demos/servers/)![Server](https://raw.githubusercontent.com/studygolang/gctt-images/master/visualizing-concurrency/servers.gif)從並發的角度看好像什麼事情都沒有發生。當然,表面平靜,內在其實風起雲湧,完成了一系列複雜的操作,只是複雜性都被隱藏了,畢竟 [Simplicity is complicated.](https://www.youtube.com/watch?v=rFejpH_tAHM)但是讓我們迴歸到並發的角度,給我們的伺服器添加一些互動功能。比如說,我們定義一個 logger 以獨立的 goroutine 的形式來記日誌,每個 handler 想要非同步地通過這個 logger 去寫資料。```gopackage mainimport ( "fmt" "net" "time")func handler(c net.Conn, ch chan string) { ch <- c.RemoteAddr().String() c.Write([]byte("ok")) c.Close()}func logger(ch chan string) { for { fmt.Println(<-ch) }}func server(l net.Listener, ch chan string) { for { c, err := l.Accept() if err != nil { continue } go handler(c, ch) }}func main() { l, err := net.Listen("tcp", ":5000") if err != nil { panic(err) } ch := make(chan string) go logger(ch) go server(l, ch) time.Sleep(10 * time.Second)}```[WebGL 動畫介面](http://divan.github.io/demos/servers2/)![Server2](https://raw.githubusercontent.com/studygolang/gctt-images/master/visualizing-concurrency/servers2.gif)這個例子就很形象地展示了伺服器處理請求的過程。我們容易發現 logger 在存在大量串連的情況下會成為效能瓶頸,因為它需要對每個串連發送的資料進行接收,編碼等耗時的操作。我們可以用上文提到的扇出模式來改進這個伺服器模型。讓我們來看看代碼和動畫效果:```gopackage mainimport ( "net" "time")func handler(c net.Conn, ch chan string) { addr := c.RemoteAddr().String() ch <- addr time.Sleep(100 * time.Millisecond) c.Write([]byte("ok")) c.Close()}func logger(wch chan int, results chan int) { for { data := <-wch data++ results <- data }}func parse(results chan int) { for { <-results }}func pool(ch chan string, n int) { wch := make(chan int) results := make(chan int) for i := 0; i < n; i++ { go logger(wch, results) } go parse(results) for { addr := <-ch l := len(addr) wch <- l }}func server(l net.Listener, ch chan string) { for { c, err := l.Accept() if err != nil { continue } go handler(c, ch) }}func main() { l, err := net.Listen("tcp", ":5000") if err != nil { panic(err) } ch := make(chan string) go pool(ch, 4) go server(l, ch) time.Sleep(10 * time.Second)}```[WebGL 動畫介面](http://divan.github.io/demos/servers3/)![Server3](https://raw.githubusercontent.com/studygolang/gctt-images/master/visualizing-concurrency/servers3.gif)在這個例子中,我們把記日誌的任務分布到了 4 個 goroutine 中,有效地改善了 logger 模組的輸送量。但是從動畫中仍然可以看出,logger 仍然是系統中最容易出現效能問題的地方。如果上千個串連同時調用 logger 記日誌, 現在的 logger 模組仍然可能會出現效能瓶頸。當然,相比於之前的實現,它的閾值已經高了很多。## 並發質數篩選法(Concurrent Prime Sieve)看夠了扇入/扇出模型,我們現在來看看具體的並行演算法。讓我們來講講我最喜歡的並行演算法之一:並行質數篩選法。這個演算法是我從 [Go Concurrency Patterns](https://talks.golang.org/2012/concurrency.slide) 這個演講中看到的。質數篩選法(埃拉托斯特尼篩法)是在一個尋找給定範圍內最大質數的古老演算法。它通過一定的順序篩掉多個質數的乘積,最終得到想要的最大質數。但是其原始的演算法在多核機器上並不高效。這個演算法的並行版本定義了多個 goroutine,每個 goroutine 代表一個已經找到的質數,同時有多個 channel 用來從 generator 傳輸資料到 filter。每當找到質數時,這個質數就會被一層層 channel 送到 main 函數來輸出。當然,這個演算法也不夠高效,尤其是當你需要尋找一個很大的質數或者在尋找時間複雜度最低的演算法時,但它的思想很優雅。```go// A concurrent prime sievepackage mainimport "fmt"// Send the sequence 2, 3, 4, ... to channel 'ch'.func Generate(ch chan<- int) { for i := 2; ; i++ { ch <- i // Send 'i' to channel 'ch'. }}// Copy the values from channel 'in' to channel 'out',// removing those divisible by 'prime'.func Filter(in <-chan int, out chan<- int, prime int) { for { i := <-in // Receive value from 'in'. if i%prime != 0 { out <- i // Send 'i' to 'out'. } }}// The prime sieve: Daisy-chain Filter processes.func main() { ch := make(chan int) // Create a new channel. go Generate(ch) // Launch Generate goroutine. for i := 0; i < 10; i++ { prime := <-ch fmt.Println(prime) ch1 := make(chan int) go Filter(ch, ch1, prime) ch = ch1 }}```[WebGL 動畫介面](http://divan.github.io/demos/primesieve/)![Prime](https://raw.githubusercontent.com/studygolang/gctt-images/master/visualizing-concurrency/primesieve.gif)這個演算法的類比動畫也同樣很優雅形象,能協助我們理解這個演算法。演算法中的 generate 這個函數發送從 2 開始的所有的整形數,傳遞給 filter 所在的 goroutine, 每個質數都會產生一個 filter 的 goroutine。 如果你在動畫連結中從上往下看,你會發現所有傳給 main 函數的數都是質數。最後總要的還是,這個演算法在 3D 類比中特別優美。## GOMAXPROCS現在,讓我們回到上文的工作者模式上。還記得我提到過這個例子是在 `GOMAXPROCS = 4` 的條件下啟動並執行嗎?這是因為所有的動畫效果都不是藝術品,它們都是用實際運行狀態類比而得的。讓我們看看 `GOMAXPROCS` 的定義:```GOMAXPROCS sets the maximum number of CPUs that can be executing simultaneously.```定義中的 CPU 指的是邏輯 CPU。我之前稍微修改了一下工作者的例子讓每個 goroutine 都做一點會佔用 CPU 時間的事情,然後我設定了不同的 `GOMAXPROCS` 值,重複運行這個例子,運行環境是一個 2 CPU, 共 24 核的機器,系統是 Linux。以下兩個圖中,第一張是運行在 1 個核上時的動畫效果,第二張是運行在 24 核上時的動畫效果。[WebGL 動畫介面 1核](http://divan.github.io/demos/gomaxprocs1/)![1Core-Worker](https://raw.githubusercontent.com/studygolang/gctt-images/master/visualizing-concurrency/gomaxprocs1.gif)[WebGL 動畫介面 24核](http://divan.github.io/demos/gomaxprocs24/)![24Core-Worker](https://raw.githubusercontent.com/studygolang/gctt-images/master/visualizing-concurrency/gomaxprocs24.gif)顯而易見,這些動畫類比花費的時間是不同的。當 `GOMAXPROCS` 是 1 的時候,只有一個工作者結束了自己的任務以後,下一個工作者才會開始執行。而在 `GOMAXPROCS` 是 24 的情況下,整個程式的執行速度變化非常明顯,相比之下,一些多工開銷變得微不足道了。儘管如此,我們也要知道,增大 `GOMAXPROCS` 的並不總是能夠提高效能,在有些情況下它甚至會使程式的效能變差。## Goroutine 泄露(Goroutines leak)Go 並發中還有什麼使我們能可視化的呢? goroutine 泄露是我能想到的一個情境。當你啟動一個 goroutine 但是它在你的代碼外陷入了錯誤狀態,或者是你啟動了很多帶有死迴圈的 goroutine 時,goroutine 泄露就發生了。我仍然記得我第一次遇到 goroutine 泄露時,我腦子裡想象的可怕情境。緊接著的周末,我就寫了 expvarmon (一個 Go 應用的資源監控工具)。現在,我可以用 WebGL 來描繪當時在我腦海中的景象了。[WebGL 動畫介面](http://divan.github.io/demos/leak/)![Goroutines leak](https://raw.githubusercontent.com/studygolang/gctt-images/master/visualizing-concurrency/leak.gif)這個圖中所有的藍線都是浪費的系統資源,並且會成為你的應用的“定時炸彈”。## 並行不是並發(Parallelism is not Concurrency)最後,我想談一下並行和並發的區別。這個主題已經在[Parallelism Is Not Concurrency](https://existentialtype.wordpress.com/2011/03/17/parallelism-is-not-concurrency/) 和 [Parallelism /= Concurrency](https://ghcmutterings.wordpress.com/2009/10/06/parallelism-concurrency/) 中探討過了,Rob Pike 在一篇[演講](https://www.youtube.com/watch?v=cN_DpYBzKso)中提到了這個問題,這是我認為必看的主題演講之一。簡單來說,**並行是指同時運行多個任務,而並發是一種程式架構的方法。**因此,帶有並發的程式並不一定是並行的,這兩個概念在一定程度上是互不相關的。我們在關於 `GOMAXPROCS` 的論述中就提到了這一點。 在這裡我不想重複上面的連結中的語言。我相信,有圖有真相。我會通過動畫類比來告訴你它們之間的不同。下面這張描述的是並行————許多任務同時運行:[WebGL 動畫介面](http://divan.github.io/demos/parallelism1/)![Parallelism1](https://raw.githubusercontent.com/studygolang/gctt-images/master/visualizing-concurrency/parallelism1.gif)這個也是並行:[WebGL 動畫介面](http://divan.github.io/demos/parallelism2/)![Parallelism2](https://raw.githubusercontent.com/studygolang/gctt-images/master/visualizing-concurrency/parallelism2.gif)但是這個是並發:![Server](https://raw.githubusercontent.com/studygolang/gctt-images/master/visualizing-concurrency/primesieve.gif)這個也是並行(嵌套的工作者):![Workers2](https://raw.githubusercontent.com/studygolang/gctt-images/master/visualizing-concurrency/workers2.gif)這個也是並發的:![pingpong100](https://raw.githubusercontent.com/studygolang/gctt-images/master/visualizing-concurrency/pingpong100.gif)# 如何產生這些動畫為了產生這些動畫,我寫了兩個程式:gotracer 和 gothree.js 庫。首先,gotracer 會做這樣的事情: 1. 解析 Go 程式中的 AST 樹並且從中插入輸出並行相關資訊的代碼,比如啟動/結束 goroutine, 建立一個 channel,發送、接收資料。 2. 運行產生的程式。 3. 分析這些輸出並且產生描述這些事件的 JSON 檔案。JSON 檔案的範例如下:![JSON](https://raw.githubusercontent.com/studygolang/gctt-images/master/visualizing-concurrency/sshot_json.png)接下來,gothree.js 使用 [Three.js](http://threejs.org/) 這個能夠用 WebGL 產生 3D 映像的的庫來繪製動畫。這種方法的使用情境非常有限。我必須精準地選擇例子,重新命名 channel 和 goroutine 來輸出一個正確的 trace。這個方法也無法關聯兩個 goroutine 中的相同但不同名的 channel,更不用說識別通過 channel 傳送的 channel 了。這個方法產生的時間戳記也會出現問題,有時候輸出資訊到標準輸出會比傳值花費更多的時間,所以我為了得到正確的動畫不得不在某些情況下讓 goroutine 等待一些時間。這就是我並沒有將這份代碼開源的原因。我正在嘗試使用 Dmitry Vyukov 的 [execution tracer](https://golang.org/cmd/trace/),它看起來能提供足夠多的資訊,但並不包含 channel 傳輸的值。也許有更好的方法來實現我的目標。如果你有什麼想法,可以通過 twitter 或者在本文下方評論來聯絡我。如果我們能夠把它做成一個協助開發人員調試和記錄 Go 程式運行情況的工具的話就更好了。如果你想用我的工具看一些演算法的動畫效果,可以在下方留言,我很樂意提供協助。Happy coding!更新: 文中提到的工具可以在[這裡](https://github.com/divan/gotrace)下載。目前它使用了 Go Execution Tracer 和打包的 runtime 來產生 trace。
via: https://divan.github.io/posts/go_concurrency_visualize/
作者:Ivan Daniluk 譯者:QueShengyao 校對:rxcai
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出
本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽
2346 次點擊