一、併發程式設計模式
在上兩篇文章中我們主要介紹了併發goroutine和channel,現在我們來介紹一下golang的併發模式,不像golang的設計模式,這裡來介紹一下常用的併發模式:
- 生成器
程式分析:msgGen()將c := make(chan string)返回給m1,在for中等待併發啟動傳送資料給m1,m1立即將資料送出並列印。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) } }
- 服務/任務
看下面程式碼:在生成器的基礎之上可以提供多個服務/任務,如上面程式碼中的m1,m2是使用同一個生成器的兩個服務/任務,而m1和m2是兩個獨立的服務/任務,我們如果拿到m1j就可以和m1j互動,拿到m2就可以和m2進行互動。func main() { m1 := msgGen() //開啟任務m1 M2 := msgGen() //開啟任務m2 for{ fmt.Println(<-m1) fmt.Println(<-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
……
……
……
- 同時等待多工:兩種方法
從上面的列印結果可以看出兩個任務是一起進行的,現在我們需要將兩個結果交替列印:
方法一:將兩個channel的資料放進一個節點中,然後在傳送到第三個channel中
下面來看是如何實現的: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即可,這裡我們叫做fanInSelectfunc 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 協議》,轉載必須註明作者和本文連結