「Golang成長之路」併發之併發模式

ice_moss發表於2021-10-06

一、併發程式設計模式

在上兩篇文章中我們主要介紹了併發goroutine和channel,現在我們來介紹一下golang的併發模式,不像golang的設計模式,這裡來介紹一下常用的併發模式:

  1. 生成器
    package main
    import (
     "fmt"
     "math/rand"
     "time"
    )
    //生成器msgGen
    func msgGen() chan string {
     c := make(chan string)
     //啟動併發,真正生成資料
     go func(){
         i := 0
         for {
         //生成時間在範圍:0~2000毫秒
             time.Sleep(time.Duration(rand.Intn(2000)) * time.Millisecond)
             c <- fmt.Sprintf("message :%d", i)
             i++
         }
     }()
     return c
    }
    func main() {
       m1 := msgGen()
       for{
          fmt.Println(<-m1)
       }
    }
    程式分析:msgGen()將c := make(chan string)返回給m1,在for中等待併發啟動傳送資料給m1,m1立即將資料送出並列印。
  1. 服務/任務
    看下面程式碼:
    func main() {
    m1 := msgGen()  //開啟任務m1
    M2 := msgGen()  //開啟任務m2
    for{
       fmt.Println(<-m1)
       fmt.Println(<-m2)
    }
    }
    在生成器的基礎之上可以提供多個服務/任務,如上面程式碼中的m1,m2是使用同一個生成器的兩個服務/任務,而m1和m2是兩個獨立的服務/任務,我們如果拿到m1j就可以和m1j互動,拿到m2就可以和m2進行互動。
    package main
    import (
    "fmt"
    "math/rand" "time")
    func msgGen(name string) chan string {
    c := make(chan string)
    go func(){
       i := 0
    for {
          time.Sleep(time.Duration(rand.Intn(2000)) * time.Millisecond)
          c <- fmt.Sprintf("service: %s, message :%d", name, i)
          i++
       }
    }()
    return c
    }
    func main() {
    m1 := msgGen("service1")
    M2 := msgGen("sercive2")
    for{
       fmt.Println(<-m1)
       fmt.Println(<-m2)
    }
    }
    列印結果:
    service: service1, message :0
    service: sercive2, message :0
    service: service1, message :1
    service: sercive2, message :1
    service: service1, message :2
    service: sercive2, message :2
    service: service1, message :3
    service: sercive2, message :3
    service: service1, message :4
    service: sercive2, message :4
    service: service1, message :5
    service: sercive2, message :5
    service: service1, message :6
    service: sercive2, message :6
    ……
    ……
    ……
  1. 同時等待多工:兩種方法
    從上面的列印結果可以看出兩個任務是一起進行的,現在我們需要將兩個結果交替列印:
    方法一:

    將兩個channel的資料放進一個節點中,然後在傳送到第三個channel中
    「Golang成長之路」併發之併發模式篇
    下面來看是如何實現的:

    func fanIn(c1, c2 chan string) chan string{
    c := make(chan string)
    go func() {
       for{
          c <- <-c1
       }
    }()
       go func() {
          for{
             c <- <-c2
          }
       }()
    return c
    }

這是完整程式碼:

package main

import (
   "fmt"
 "math/rand" "time")

func msgGen(name string) chan string {
   c := make(chan string)
   go func(){
      i := 0
  for {
         time.Sleep(time.Duration(rand.Intn(2000)) * time.Millisecond)
         c <- fmt.Sprintf("service: %s, message :%d", name, i)
         i++
      }

   }()
   return c

}

func fanIn(c1, c2 chan string) chan string{
   c := make(chan string)
   go func() {
      for{
         c <- <-c1
      }
   }()
      go func() {
         for{
            c <- <-c2
         }
      }()
   return c

}
func main() {
   m1 := msgGen("service1")
   m2 := msgGen("sercive2")
   m := fanIn(m1, m2)
   for{
      fmt.Println(<-m)
   }

}

方法二:
使用select對多個channel同時接收,此時只需要開一個goroutine即可,這裡我們叫做fanInSelect

func fanInSelect(c1, c2 chan string) chan string{
   c := make(chan string)
   go func() {
      for{
         select{
         case m := <- c1:
            c <- m
         case m := <- c2:
            c <- m
         }
      }
   }()
   return c
   func main() {
   m1 := msgGen("service1")
   m2 := msgGen("sercive2")
   m := fanInSelect(m1, m2)
   for{
      fmt.Println(<-m)
   }
}

列印結果:
service: sercive2, message :0
service: service1, message :0
service: sercive2, message :1
service: service1, message :1
service: sercive2, message :2
service: sercive2, message :3
service: sercive2, message :4
service: service1, message :2
service: sercive2, message :5
service: sercive2, message :6
service: service1, message :3
……
……

方法一,方法二對比:
對比兩種方法,方法一想要開兩個goroutine(如果有多個引數就需要開很多goroutine),而方法二隻需要開一個goroutine即可;當我們知道有具體的引數時(channel),使用方法二會更好,在不知道具體有多少個goroutine的情況下使用方法一更好。下面來看看方法一的最佳化在哪裡:


func fanIn(chs ...chan string) chan string{   //chs ...引數限制,可隨意增減
    c := make(chan string)
    for _, ch := range chs{  //第一個for將每一個引數取出,每一個channel需要開一個goroutine
            go func() {
                for { //第二個for源源不斷的將資料傳出
                    c <- <-ch
                }
            }()
        }
    return c
}

列印結果:
service: sercive2, message :0
service: sercive2, message :1
service: sercive2, message :2
service: sercive2, message :3
service: sercive2, message :4
service: sercive2, message :5
service: sercive2, message :6
service: sercive2, message :7
service: sercive2, message :8
service: sercive2, message :9

注意:列印結果全是service2
原因:我們每次從chs中取出一個channel給ch,執行到關鍵字”go”就return ch,接著進行將chs中的第二個channel的給ch,接著return,此時第一個ch已經被迭代成了第二ch了,所以當goroutine真正的執行時,傳入c中的資料都來自於最新的ch。
解決方法:增加變數來儲存chs中的資料,如 chcapy := ch

func fanIn(chs ...chan string) chan string{
   c := make(chan string)
   for _, ch := range chs{  //第一個for將每一個引數取出,每一個channel需要開一個goroutine
  chcapy := ch
         go func() {
            for { //第二個for源源不斷的將資料傳出
  c <- <- chcapy
            }
         }()
      }
   return c
}

或者將在函式式func()增加引數(這裡需要了解go語言的傳參,見文章「Golang成長之路」基礎語法go func(in chan string) {……}()

func fanIn(chs ...chan string) chan string{
   c := make(chan string)
   for _, ch := range chs{  //第一個for將每一個引數取出,每一個channel需要開一個goroutine
  go func(in chan string) {
            for { //第二個for源源不斷的將資料傳出
  c <- <- in
            }
         }(ch)
      }
   return c
}

再來看看呼叫:
可隨意增加引數

三個引數

func main() {
   m1 := msgGen("service1")
   m2 := msgGen("sercive2")
   m3 := msgGen("service3")
   m := fanIn(m1, m2, m3)
   for{
      fmt.Println(<-m)
   }
}

四個引數:

func main() {
   m1 := msgGen("service1")
   m2 := msgGen("sercive2")
   m3 := msgGen("service3")
   m4 := msgGen("service4")
   m := fanIn(m1, m2, m3, m4)
   for{
      fmt.Println(<-m)
   }
}
本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章