一種 golang 實現 多協程任務處理的套路
來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。# 一種 golang 實現 多協程任務處理的套路## 那麼是什麼樣的任務呢,一般是在生產者-消費者模式的消費者進程 ,舉幾個例子1. 消費kafka 資料2. 消費redis 資料3. 輪詢處理資料庫資料4. ...## 下面來分析一下1. 商務邏輯處理協程 到底多少個呢 ?處理一個資料 就 go 一個嗎,也可以不過有點粗暴,協程也不是越多越好,調度也是要好效能的所以還是控制一下,一般吧 弄個cpu * 2 就差不多了(runtime.NumCPU() *2)2. 擷取資料協程由於我要分析的例子 都是一個 for 迴圈 不停讀取資料 交個任務處理協程,所以這裡就 用一個協程3. 進程如何關閉總不能kill -9 粗暴處理吧,這樣容易造成資料異常或者丟資料,一般都是 捕捉 訊號signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)直接上代碼```golangpackage mainimport ("fmt""os""os/signal""runtime""sync/atomic""syscall""time")type TaskData struct {}type Service struct {capacity inttasks chan *TaskDatanumThread intcloseChans chan struct{}stopFlag int32loopStopChan chan struct{}}func NewService(capacity int) *Service {service := &Service{}service.capacity = capacityservice.numThread = runtime.NumCPU() * 2service.tasks = make(chan *TaskData, capacity)service.stopFlag = 0service.closeChans = make(chan struct{}, service.numThread)service.loopStopChan = make(chan struct{})return service}func (this *Service) Stop() {atomic.StoreInt32(&this.stopFlag, 1)<-this.loopStopChanclose(this.tasks)for i := 0; i < this.numThread; i++ {<-this.closeChans}}func (this *Service) Run() {for i := 0; i < this.numThread; i++ {go this.run(i)}go this.LoopConsume()}func (this *Service) run(i int) {fmt.Println("go run:", i)loop:for {select {case task, ok := <-this.tasks:if ok {//#TODO processfmt.Println("process", task)} else {break loop}}}this.closeChans <- struct{}{}}func (this *Service) LoopConsume() {fmt.Println("loop")for atomic.LoadInt32(&this.stopFlag) == 0 {//TODO ReadDatatask := &TaskData{}this.tasks <- taskfmt.Println("consume.")time.Sleep(time.Second * 2)}this.loopStopChan <- struct{}{}}func main() {service := NewService(100)go service.Run() //啟動程式處理c := make(chan os.Signal)signal.Notify(c, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)s := <-c //等待關閉訊號fmt.Println(s)service.Stop() //關閉service fmt.Println("exit :D")}```## 思考 1:``` golangservice.stopFlag service.closeChansservice.loopStopChan ```這幾個變數幹什麼用的,是為了安全退出程式用的 ### 1. stopFlag> 首先 要退出 LoopConsume 大迴圈 用什麼去通知呢,用channel 也可以,就是要配合select 使用,但是用原子標記是不是更簡潔呢? 所以 stopFlag 是為了退出 LoopConsume 用的### 2. closeChans> 由於我們go 了很多個協程,那麼要監聽每一個協程退出,就需要多個channel 去接收```for i := 0; i < this.numThread; i++ {<-this.closeChans }```> 這段代碼的意思就是等待所有 處理協成退出### 3. loopStopChan> 這個又是幹什麼的呢,同樣也是處理協程退出的 只不過是 LoopConsume,為什麼不 closeChans 大小再加一個 而變成這樣呢```service.closeChans = make(chan struct{}, service.numThread+1)``````func (this *Service) Stop() {atomic.StoreInt32(&this.stopFlag, 1)close(this.closeChans)for i := 0; i < this.numThread+1; i++ {<-this.closeChans}}```這麼做會發生什麼呢?,假如這樣 一旦執行了stop 那麼this.stopFlag = 1,但是 LoopConsume 可能還在從 //TODO ReadData 擷取資料階段當執行了 close(this.tasks) ,此時 恰好又要執行 this.tasks <- task,但是此時 tasks 已經關閉,那麼就會panic 其實在整個例子裡 LoopConsume 就相當於一個生產者,而run 相當於一個消費者,我們是不是應該先關不生產者 等待 消費者 消費完了 再退出呢,毫無疑問 肯定是的,所以就要有一個channel 等 生產者 退出了 再發送 channel 去 讓消費者退出,所以單獨用一個 loopStopChan## 思考2:```func (this *Service) LoopConsume() {fmt.Println("loop")for atomic.LoadInt32(&this.stopFlag) == 0 {//TODO ReadDatatask := &TaskData{}this.tasks <- taskfmt.Println("consume.")time.Sleep(time.Second * 2)}this.loopStopChan <- struct{}{}}```這段代碼其實就是不停的擷取資料,我這裡沒有寫擷取資料的部分,因為這個是和業務相關的,舉個實際點的例子 比如 比如 讀取mysqlSELECT ID, * FROM DATA WHERE ID > OFFSET LIMIT N;每次 從OFFSET 位置讀取 N 條資料,讀取後 如果擷取的條數 為 num ,若num等於 N , 那麼 OFFSET += N 繼續 read,否則 說明資料不夠了 ,則,OFFSET += num,並且 sleep n 秒 (避免沒有資料的時候空跑)上虛擬碼```func (this *Service) LoopConsume() {fmt.Println("loop")for atomic.LoadInt32(&this.stopFlag) == 0 {rows:= Read(offset)if rows 行數 == N {task := &TaskData{}this.tasks <- taskoffset + = N}else{time.Sleep(time.Second * 20)offset += rows 行數}}this.loopStopChan <- struct{}{}}```這裡有沒有發現問題呢,假如程式剛好進入了time.Sleep(time.Second * 20) 這裡呢,此時stop 豈不是 要等待20s 可是其實進程已經很閑了,有什麼辦法解決 呢,還是可以用標記的方法,一段程式進入sleep 可以設定一個標記上虛擬碼```func (this *Service) LoopConsume() {fmt.Println("loop")for atomic.LoadInt32(&this.stopFlag) == 0 {atomic.StoreInt32(&this.forcestopFlag, 0)rows:= Read(offset)if rows 行數 == N {task := &TaskData{}this.tasks <- taskoffset + = N}else{atomic.StoreInt32(&this.forcestopFlag, 1)time.Sleep(time.Second * 20)offset += rows 行數}}this.loopStopChan <- struct{}{}}func (this *Service) Stop() {atomic.StoreInt32(&this.stopFlag, 1)if atomic.LoadInt32(&this.forcestopFlag) == 0{<-this.loopStopChan //只有當forcestopFlag = 0 的時候才需要等待 LoopConsume退出}close(this.tasks)for i := 0; i < this.numThread; i++ {<-this.closeChans}}```### 思考3:此模型 run 裡面 或者 LoopConsume 還可以 go 協程出來嗎,顯然不行,因為一旦go 出來 了,那麼現有的 stop 就失效了,因為無法擷取這些協程是否退出。其實我覺得也沒有必要 再go 一個出來,因為LoopConsume 一般是讀 ,速度比 業務處理的要高, 如果這個不滿足你的實際業務需求,你可以 go 多個 LoopConsume ,同樣把 loopStopChan 也弄成 長度為 N 的channel<-this.loopStopChan 變成這樣```for i := 0; i <N ; i++ {<-this.loopStopChan}```再極端 你的業務非得 在 LoopConsume go 一個或者多個協程,那麼你得思考 該怎麼同步了,至於用什麼方法,得好好思考了,可以提出來大夥一起討論討論同理在 run 裡面你也想 go 一個或者多個協程, 還是一樣得想辦法考慮同步問題## 總結這個小service 只是一種多協程處理任務的套路,常用在生產者消費者模型的消費者進程。對於對效能要求比較高的可能不適合,比如這裡的 協程數是固定的,可以改進成伸縮的動態變化,代碼寫的比較簡單,一些錯誤之處 還望各位 大神多多指正,歡迎討論。766 次點擊 ∙ 1 贊