入門goroutine並發設計模式以及goroutine視覺化檢視

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

Daisy-Chain

首先,為了防止過於枯燥,我先列出我最喜歡的一個模式:Daisy-Chain。這個模式比較複雜,對go的並發編程不太熟悉的同學,可以先看下面的模式。然後回過頭來看這個。

daisy chain會建立很多channel,然後把這些channel首尾相接級聯起來,組成一條單向鏈,每個channel都在處理不同的子任務,最後的結果在鏈的末端輸出。這是在2012年的golang talks中由Rob Pike提出的:

func f(left, right chan int) {    // 這個函數就把right的輸出和left的輸入聯絡起來了。    left <- 1 + <-right}func main() {    const n = 10000    leftmost := make(chan int)    right := leftmost    left := leftmost    // 建立長度為n的daisy鏈    for i := 0; i < n; i++ {        right = make(chan int)        go f(left, right)        left = right    }    // 在鏈的最右端輸入1,那麼最左端就會得到10001    go func(c chan int) { c <- 1 }(right)    fmt.Println(<-leftmost)}

整個過程類似:

那麼這個模式有什麼用呢?它可以用來處理迭代演算法,使得部分迭代運算並發執行。只要迭代的每個階段都是相互獨立的即可。比如,計算質數:

import (       "fmt"       "os"       "runtime/trace"       "time")func Generate(ch chan<- int) {       for i := 2; ; i++ {           ch <- i           // 這是為了方便gotrace繪圖           time.Sleep(10 * time.Millisecond)       }}func Filter(ch <-chan int, out chan<- int, prime int) {       for {           i := <-ch           if i%prime != 0 {               out <- i           } else {               fmt.Printf("[%d] filter out %d\n", prime, i)           }       }}func main() {    // 這些也是gotrace要求插入的代碼。下同    trace.Start(os.Stderr)       ch := make(chan int)       go Generate(ch)       for i := 0; i < 10; i++ {           prime := <-ch   // step1           fmt.Println(prime)           out := make(chan int)   // step2            go Filter(ch, out, prime)  // step3           ch = out   // step4       }       trace.Stop()}

仔細分析上面的代碼,它的功能就是輸出前10個正整數質數。至於細節就讓我們一步步分析看看:
首先,Generate從2開始遍曆正整數,並且在一開始就被放入goroutine裡了。結果會放在ch裡;
然後,在main中啟動一個for迴圈,在迴圈的每個step1,都會從ch中讀出一個質數。2當然是質數,但是後面每一步從ch中讀取的都是質數嗎?且看下面的代碼。
然後,step2會建立一個新channel out(類似上例的right),ch和它作為輸入和輸出建立一個Filter的goroutine,專門過濾能被step1的prime整除的數。所以在out中輸出的都是不會被prime整除的數。
最後在關鍵的step4,out變成下一個ch。相當於增加了一節chain的長度。而ch在每個迴圈中輸出的第一個數,都是被之前的所有質數無法整除的數,即下一個質數。
輸出日誌如下:

23[2] filter out 45[2] filter out 67[2] filter out 8[3] filter out 9[2] filter out 1011[2] filter out 1213[2] filter out 14[3] filter out 15[2] filter out 1617[2] filter out 1819[2] filter out 20[3] filter out 21[2] filter out 2223[2] filter out 24[5] filter out 25[2] filter out 26[3] filter out 27[2] filter out 2829

為了更直觀的展示整個過程,我用divan大神的gotrace工具畫出了goroutine的3d互動圖:

其中每個紅色豎線表示一個goroutine,時間軸是從上到下的,所以紅線越長表示goroutine期間越長,也說明它產生的越早。
可以看到,最早的一個goroutine獲得的數字是3,4,…… 29,因為2已經被輸出了,所以是3到29,然後下一個goroutine獲得的就是5,7,9,…… 29,因為3被輸出,而偶數都被過濾了。以此類推,最後輸出的就是前10個質數。
需要指出的是,這個演算法並不是最高效的,但卻是非常優雅的。

關於gotrace的安裝和使用,請移步這裡。我是根據他的方法給go1.6.3打了補丁後,就能使用了。

好了,下面我們換些基礎的模式講一下:

Ping-pong

顧名思義,就是由2個goroutine相互踢皮球組成的模式。是在2013年的golang talks中提出的。儘管它非常簡單,但是卻方便我們理解go的並發編程概念。

代碼如下:
用一個int表示ball(球),管道表示table(桌子),兩個goroutine就是2個運動員, 分別編號為1和2。

func main() {    var Ball int    table := make(chan int)    go player("2", table)    go player("1", table)    // 首先把球放到“桌上”    table <- Ball    time.Sleep(1 * time.Second)    // 1s後比賽結束……    <-table}func player(id string, table chan int) {    for {        ball := <-table        log.Printf("%s got ball[%d]\n", id, ball)        time.Sleep(50 * time.Millisecond)        log.Printf("%s bounceback ball[%d]\n", id, ball)        ball++        table <- ball    }}

輸出如下:

1 got ball[0]1 bounceback ball[0]2 got ball[1]2 bounceback ball[1]1 got ball[2]1 bounceback ball[2]2 got ball[3]2 bounceback ball[3]1 got ball[4]1 bounceback ball[4]2 got ball[5]2 bounceback ball[5]

代碼簡潔易懂,很好理解(看不懂的同學請不要拍我)。
下面,我們增加一位選手,讓3個運動員一塊打球

    go player("2", table)    go player("3", table)    go player("1", table)

這下子熱鬧了,輸出如下:

1 got ball[0]1 bounceback ball[0]2 got ball[1]2 bounceback ball[1]3 got ball[2]3 bounceback ball[2]1 got ball[3]1 bounceback ball[3]2 got ball[4]2 bounceback ball[4]3 got ball[5]3 bounceback ball[5]1 got ball[6]1 bounceback ball[6]2 got ball[7]2 bounceback ball[7]3 got ball[8]3 bounceback ball[8]

看3個人有條不紊的相互擊球。此時處女座一定非常滿意,但是對於習慣了並發隨機性的程式員來說,這實在有些過於美好:為什麼它們的順序如此協調,為什麼1總是給2,2給3,3給1,而不是其他順序呢?

劃重點了啊:

The answer is because Go runtime holds waiting FIFO queue for receivers, that is goroutines ready to receive on the particular channel

即,對於接收channel內容的goroutines來說,go的runtime會把它們分配到一個FIFO隊列中,所以這些goroutines只能按照既定的順序接收channel的內容,而不會弄亂。所以即使建立上百個palyers,順序依然是固定的。go實在是太貼心了,有不有!

Fan-In

也叫“扇入”,應該是並發編程裡面比較普通的一個模式了。fan-in會從多個管道讀取輸入,並匯總到一個channel輸出,形象的比喻如:

範例程式碼如下

import (       "fmt"       "math/rand"       "os"       "runtime/trace"       "time")func main() {       trace.Start(os.Stderr)       c := fanIn(boring(1), boring(2))       for i := 0; i < 10; i++ {           fmt.Println(<-c)       }       fmt.Println("You're both boring; I'm leaving.")       trace.Stop()}func fanIn(input1, input2 <-chan int) <-chan int {       c := make(chan int)       go func() { for {c <- <-input1} }()       go func() { for {c <- <-input2} }()       return c}func boring(msg int) <-chan int {       c := make(chan int)       go func() { // We launch the goroutine from inside the function.           for i := 0; ; i++ {               c <- msg*1000 + i               time.Sleep(time.Duration(rand.Intn(1e3)) * time.Millisecond)           }       }()       return c // Return the channel to the caller.}

輸出為:
2000
2001
1001
2002
1002
2003
1003
2004
1004
gotrace輸出為(注意這是兩次獨立的運行結果):

可以看到,兩次的結果都匯入了main線程,並且順序輸出,沒有遺失資料,也沒有死結。

當然,簡單的情況,用select也可以。

select設計的目的就是在channel中間通訊,誰的資料先到達,哪個case分支先執行。

c1 := boring(1)c2 := boring(2)for i := 0; i < 10; i++ {    select {    case v := <-c1:        fmt.Println(v)    case v := <-c2:        fmt.Println(v)    }}

Workers

也叫FanOut(扇出),和扇入模式相反,工作模式是一個管道分發任務,多個goroutines來執行。
範例程式碼如下:

import (    "fmt"    "os"    "runtime/trace"    "sync"    "time")func worker(ch <-chan int, wg *sync.WaitGroup) {    defer wg.Done()    for {        task, ok := <-ch        if !ok {            return        }        time.Sleep(20 * time.Millisecond)        fmt.Println("processing task", task)    }}func pool(wg *sync.WaitGroup, workers, tasks int) {    ch := make(chan int)    for i := 0; i < workers; i++ {        time.Sleep(1 * time.Millisecond)        // spawn出很多worker線程        go worker(ch, wg)    }    for i := 0; i < tasks; i++ {        time.Sleep(10 * time.Millisecond)        // 開始分發任務,被啟用的workers開始工作了        ch <- i    }    close(ch)}func main() {    trace.Start(os.Stderr)    var wg sync.WaitGroup    wg.Add(36)    go pool(&wg, 36, 36)    wg.Wait()    trace.Stop()}

代碼略長,但是邏輯其實非常清晰。我在注釋中也稍作了說明。
注意(劃重點了),close(ch)在這裡很關鍵,它確定了每個worker退出的節點。當channel中的內容為空白,同時它已經被close時,task, ok := <- ch返回的ok==false,此時通知worker退出,wg標記完成,當所有的worker都完成時,wg.Wait()完成,轉入下一行執行。
在golang中,main不會自動等待所有子進程完成,如果沒有退出檢查,main進程會閃退,所有的子進程也會隨之強制退出,所以在main裡必須有退出檢測機制,前幾個例子我們使用的是time.Sleep和for迴圈,這裡我們使用了WaitGroup。

gotrace結果如下:

圓柱體中心就是main進程中產生的pool進程,圍繞它的是36個worker進程。藍色箭頭表示pool每隔10ms分發的任務,它們都被worker處理了。

Servers

server模式和fan_out類似,只不過它的worker線程是按需產生的,並且工作處理完畢後就釋放。所以這種模式常應用到網站伺服器上。在主進程中,有一個for迴圈,Accept函數一直阻塞著迴圈的進行,一旦有新的請求過來,Accept就會產生一個connection,然後主進程就建立一個子進程處理這個connection以及其他邏輯。

範例程式碼如下:

import (       "fmt"       "net"       "os"       "runtime/trace"       "time")func handler(c net.Conn, ch chan int) {   ch <- len(c.RemoteAddr().String())   time.Sleep(10 * time.Microsecond)   c.Write([]byte("ok"))   c.Close()}func logger(ch chan int) {       for {           time.Sleep(1500 * time.Millisecond)           fmt.Println(<-ch)       }}func server(l net.Listener, ch chan int) {       for {           c, err := l.Accept()           if err != nil {               continue           }           go handler(c, ch)       }}func main() {       trace.Start(os.Stderr)       l, err := net.Listen("tcp", ":5000")       if err != nil {           panic(err)       }       ch := make(chan int)       go logger(ch)       go server(l, ch)       time.Sleep(10 * time.Second)       trace.Stop()}

可以看到,主進程產生了一個tcp串連,啟動了server和logger兩個子進程。server用來監聽外網的請求,一旦請求過來,就會產生一個handler進程,用來處理connection。同時,handler還會通過管道和logger通訊,logger負責非同步記錄相應日誌。

這個程式運行時的輸入需要類比外部請求來產生,為此我寫了一個指令碼:

#!/bin/shi=0while [[ $i -lt 20 ]];do    # 通過nc發起tcp請求。每秒請求一次    echo "hello "$i | nc localhost 5000    sleep 1    ((++i))done

運行時,先啟動這個指令碼,然後啟動server或gotrace。

gotrace的運行結果如下:

可以看到,儘管程式運行了10s,但是只處理了6個請求。這是因為logger佔用了管道太長時間,使得handler的已耗用時間也延長到了1.5s以上。

為瞭解決這個問題,我們正好藉助上面介紹的Worker模式,提高logger的並發性。

Server + Worker

import (           "fmt"           "net"           "os"           "runtime/trace"           "time")func handler(c net.Conn, ch chan int) {       ch <- 0       time.Sleep(50 * time.Microsecond)       c.Write([]byte("ok"))       c.Close()}func logger(wch chan int) {       for {           fmt.Println(<-wch)           // 這裡主要耗時           time.Sleep(1500 * time.Millisecond)       }}func pool(ch chan int, n int) {       wch := make(chan int)       for i := 0; i < n; i++ {           go logger(wch)       }       for {           wch <- <-ch       }}func server(l net.Listener, ch chan int) {       for {           c, err := l.Accept()           if err != nil {               continue           }           go handler(c, ch)       }}func main() {       trace.Start(os.Stderr)       l, err := net.Listen("tcp", ":5000")       if err != nil {           panic(err)       }       ch := make(chan int)       go pool(ch, 36)       go server(l, ch)       time.Sleep(10 * time.Second)       trace.Stop()}

其中pool函數跟上例類似,就是產生(spawn)很多worker,然後handle中產生的資料會先進入pool,由pool再分配給這些workers。

3D圖如下:

可以看到,此時server正好處理了10個請求。不再被logger拖延了。

Concurrency & Parallelism

注意我的題目是並發(concurrent)設計模式。那麼並發和並行到底啥區別??

  • Concurrency: A condition that exists when at least two threads are making progress. A more generalized form of parallelism that can include time-slicing as a form of virtual parallelism.
    Concurrency(並發性):是一種廣義的並行。在concurrence的語境下,兩個線程/任務可以表面上看起來“像是”並行,但其實機器只有一個核,它們只是分享了時間塊。當然,在多核的情況下,它可以是並行的。

  • Parallelism(並行性):A condition that arises when at least two threads are executing simultaneously.
    這個就是狹義的並行,即線程、任務必須是同時進行的,否則不算parallelism。

聯繫我們

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