Golang協程併發的流水線模型

屈天航發表於2020-11-18

背景

最近由於效能問題,後端服務一直在做python到golang的遷移和重構。go語言精簡優雅,既有編譯型語言的嚴謹和高效能,又有解釋型語言的開發效率,出色的併發效能也是go區別於其他語言的一大特色。go的併發程式設計程式碼雖然簡單,但重在其併發模型和流程的設計。所以這裡總結下golang協程併發常用的流水線模型。

簡單的流水線思維

流水線模式並不是什麼新奇的概念,但是它能極大地提高生產效率。比如實際生活中的汽車生產流水線,流水線上的每一個流程負責不同的工作,比如第一個流程是拼裝車身,第二個流程是安裝發動機,第三個流程是裝輪胎...,這些步驟我們可以類比成go併發流程中的協程,每一個協程就是一個任務。流水線上面傳遞的車身、發動機、輪胎,這些我們可以類比成協程間需要傳遞的資料,而在這些流程(協程)間傳遞這些配件(資料),自然就要通過傳送帶(channel)。在流水線上,我們裝四個輪胎肯定不是一個一個來裝的,肯定是有四個機械臂同時來裝。因此裝輪胎這個步驟我們有4個協程在併發工作來提高效率。這麼一來,流水線模型的基本要素就構成了。
Golang的併發模型靈感其實都來自我們生活,對程式而言,高的生產效率就是高的效能。在Golang中,流水線由多個流程節點組成,流程之間通過channel連線,每個流程節點可以由多個同時執行的goroutine組成。
image.png

如何構造流水線

有了流水線模式的思維,接下來就是如何構造流水線了。簡單來說,其實就是通過channel將任務流程連線起來,兩個相鄰的流程互為生產者和消費者,通過channel進行通訊。耗時的流程可以將任務分散到多個協程來執行。
我們先來看一個最簡單的流水線,如下圖,A是生產者流程,B是它的消費流程,同時又是C的生產者流程。A,B,C三個協程直接,通過讀寫channel進行通訊。
image.png

那如果此時B流程可以將a channel中的任務併發執行呢,很簡單,我們只需要起多個B協程就可以了。如下圖。
image.png

總之,我們構造流水線併發的思路是關注資料的流動,資料流動的過程交給channel,channel兩端資料處理的每個環節都交給goroutine,這個流程連起來,就構成了流水線模型。

關於channel

為什麼我們可以選擇channel來進行協程間的通訊呢,協程之間又是怎麼保持同步順序呢,當然這都要歸功於channel。channel是go提供的程式內協程間的通訊方式,它是協程/執行緒安全的,channe的讀寫阻塞會導致協程的切換。
channel的操作和狀態組合可以有以下幾種情況:
image.png

**有1個特殊場景**:當`nil`的通道在`select`的某個`case`中時,這個case會阻塞,但不會造成死鎖。

channel不僅可以保證協程安全的資料流動,還可以保證協程的同步。當有併發問題時,channel也是我們首先應該想到的資料結構。不過,當使用有緩衝區的channel時,才能達到協程併發的效果,並且生產者和消費者的協程間是相對同步的。使用無緩衝區的channel時,是沒有併發效果的,協程間是絕對同步的,生產者和消費者必須同時寫和讀協程才能執行。
channel關注的是資料的流動,這種場景下都可以考慮使用channel。比如:訊息傳遞、訊號廣播、任務分發、結果彙總、同步與非同步、併發控制... 更多的不在這裡贅述了,總之,Share memory by communicating, don't communicate by sharing memory.

流水線模型例項

舉個簡單栗子,計算80000以內的質數並輸出。
這個例子如果我們採用非併發的方式,就是for迴圈80000,挨個判斷是不是素數再輸出。不過如果我們採用流水線的併發模型會更高效。
從資料流動的角度來分析,需要遍歷生成1-80000的數字到一個channel中,數字判斷是否為素數,輸出結果到一個channel中。因此我們需要兩個channel,channel的兩端就設計成協程即可。
1、遍歷生成原始80000個資料(生產者)
2、計算這80000個資料中的素數(生產者+消費者)
3、取結果輸出(消費者)

package gen_channel
import "fmt"
import "time"
func generate_source(data_source_chan chan int) {
   for i := 1; i <= 80000; i++ {
      data_source_chan <- i
   }
   fmt.Println("寫入協程結束")
   close(data_source_chan)
}
func generate_sushu(data_source_chan chan int, data_result_chan chan int, gen_chan chan bool) {
   for num:= range data_source_chan {
      falg := true
 for i := 2; i < num; i++ {
         if num%i == 0 {
            falg = false
 break }
      }
      if falg == true {
         data_result_chan <- num
      }
   }
   fmt.Println("該協程結束")
   gen_chan <- true
}
func workpool(data_source_chan chan int, data_result_chan chan int, gen_chan chan bool, gen_num int){
   // 開啟8個協程
 for i := 0; i < gen_num; i++ {
      go generate_sushu(data_source_chan, data_result_chan, gen_chan)
   }
}
func Channel_main() {
   data_source_chan := make(chan int, 2000)
   data_result_chan := make(chan int, 2000)
   gen_chan := make(chan bool, 8)
   time1 := time.Now().Unix()
   go generate_source(data_source_chan)
   // 協程池,任務分發
   workpool(data_source_chan, data_result_chan, gen_chan, 8)
   go func() {
      for i := 0; i < 8; i++ {
         <-gen_chan
      }
      close(data_result_chan)
      fmt.Println("spend timeis ", time.Now().Unix()-time1)
   }()
   for date_result := range data_result_chan {
      fmt.Println(date_result)
   }
}

相關文章