工作中檢視專案程式碼,發現會存在使用 GO 語言做併發的時候出現各種各樣的異常情況,有的輸出結果和自己期望和設計的不一致,有的是程式直接阻塞住,更有甚者直接是程式 crash 掉。
實際上,出現上述的情況,還是因為我們對於 GO 語言的併發模型和涉及的 GO 語言基礎不夠紮實,誤解了語言的用法。
那麼,對於 GO 語言的併發模式,我們一起來梳理一波。 GO 語言常見的併發模式有這些:
- 建立模式
- 退出模式
- 管道模式
- 超時模式和取消模式
在 GO 語言裡面,我們們使用使用併發,自然離不開使用 GO 語言的協程 goroutine,通道 channel 和 多路複用 select,接下來就來看看各種模式都是如何去搭配使用這三個關鍵原語的
建立模式
使用過通道和協程的朋友對於建立模式肯定不會模式,這是一個非常常用的方式,也是一個非常簡單的使用方式:
- 主協程中呼叫 help 函式,返回一個通道 ch 變數
- 通道 ch 用於主協程和 子協程之間的通訊,其中通道的資料型別完全可以自行定義
type XXX struct{...}
func help(fn func()) chan XXX {
ch := make(chan XXX)
// 開啟一個協程
go func(){
// 此處的協程可以控制和外部的 主協程 透過 ch 來進行通訊,達到一定邏輯便可以執行自己的 fn 函式
fn()
ch <- XXX
}()
}
func main(){
ch := help(func(){
fmt.Println("這是GO 語言 併發模式之 建立模式")
})
<- ch
}
退出模式
程式的退出我們應該也不會陌生,對於一些常駐的服務,如果是要退出程式,自然是不能直接就斷掉,此時會有一些連線和業務並沒有關閉,直接關閉程式會導致業務異常,例如在關閉過程中最後一個 http 請求沒有正常響應等等等
此時,就需要做優雅關閉了,對於協程 goroutine 退出有 3 種模式
- 分離模式
- join 模式
- notify-and-wait 模式
分離模式
此處的分離模式,分離這個術語實際上是執行緒中的術語,pthread detached
分離模式可以理解為,我們們建立的協程 goroutine,直接分離,建立子協程的父協程不用關心子協程是如何退出的,子協程的生命週期主要與它執行的主函式有關,我們們 return 之後,子協程也就結束了
對於這類分離模式的協程,我們們需要關注兩類,一種是一次性的任務,我們們 go 出來後,執行簡單任務完畢後直接退出,一種是常駐程式,需要優雅退出,處理一些垃圾回收的事情
例如這樣:
- 主程式中設定一個通道變數 ch ,型別為 os.Signal
- 然後主程式就開始各種建立協程執行自己的各種業務
- 直到程式收到了 syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT 任意一個訊號的時候,則會開始進行垃圾回收等清理工作,執行完畢後,程式再進行退出
func main(){
ch := make(chan os.Signal)
signal.Notify(c, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
// ...
// go 程式執行其他業務
// ...
for i := range ch {
switch i {
case syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT:
// 做一些清理工作
os.Exit(0)
}
}
}
join 模式
看到這個關鍵字,是不是也似曾相識,和執行緒貌似很像,例如 執行緒中 父執行緒可以透過 pthread_join 來等待子執行緒結束,並且還可以獲取子執行緒的結束狀態
GO 語言中等待子協程退出並且獲取子協程的退出狀態,我們們就可以使用通道 channel 的方式來進行處理
例子1
等待一個子協程退出,並獲取退出狀態
- 主協程中呼叫 help 方法得到一個 ch 通道變數,主協程阻塞著讀 ch
- help 中開闢一個子協程去執行傳入的 fn 回撥函式,並傳參為 ok bool
- 實際 fn 函式判斷傳參 ok 是否是 true,若不是則返回具體的錯誤資訊,若是 true 則返回 nil
func help(f func(bool) error, ok bool) <-chan error {
ch := make(chan error)
go func() {
ch <- f(ok)
}()
return ch
}
func fn(ok bool) error {
if !ok {
return errors.New("not ok ... ")
}
return nil
}
func main() {
ch := help(fn, true)
fmt.Println("help 111")
err := <-ch
fmt.Println("help 111 done ", err)
ch = help(fn, false)
fmt.Println("help 222")
err = <-ch
fmt.Println("help 222 done ", err)
}
看上如上程式,我們就可以知道,第一次呼叫 help(fn , true)
,主協程等待子協程退出的時候,會得到一個錯誤資訊,為 not ok ...
, 第二次呼叫 help(fn , false)
的時候,返回的 err 是一個 nil
透過上述這種方式,主協程不僅可以輕易的等待一個子協程退出,還可以獲取到子協程退出的狀態
那麼,主協程如果是等待多個協程退出呢?需要如何處理?
例子2
主協程等待多個協程退出我們們就需要使用到 GO 中的 sync.WaitGroup
- 使用 help 函式,傳入回撥函式,引數1 bool,引數2 int ,其中引數 2 表示開闢子協程的個數,返回值為一個無緩衝的 channel 變數,資料型別是 struct{}
- 使用 var wg sync.WaitGroup ,開闢子協程的時候記錄一次 wg.Add(1),當子協程退出時 ,記錄退出 wg.Done()
- help 中再另起一個協程 wg.Wait() 等待所有子協程退出,並將 ch 變數寫入值
- 主協程阻塞讀取 ch 變數的值,待所有子協程都退出之後,help 中寫入到 ch 中的資料,主協程就能馬上收到 ch 中的資料,並退出程式
func help(f func(bool)error, ok bool, num int)chan struct{}{
ch := make(chan struct{})
var wg sync.WaitGroup
for i:=0; i<num; i++ {
wg.Add(1)
go func(){
f(ok)
fmt.Println(" f done ")
wg.Done()
}()
}
go func(){
// 等待所有子協程退出
wg.Wait()
ch <- struct{}{}
}()
return ch
}
func fn(ok bool) error{
time.Sleep(time.Second * 1)
if !ok{
return errors.New("not ok ... ")
}
return nil
}
func main(){
ch := help(fn , true)
fmt.Println("help 111")
<- ch
fmt.Println("help 111 done ",err)
}
notify-and-wait 模式
可以看到上述模式,都是主協程等待一個子協程,或者多個子協程結束後,主協程再進行退出,或者處理完垃圾回收後退出
那麼如果主協程要主動通知子協程退出,我們應該要如何處理呢?
同樣的問題,如果主協程自己退出了,而沒有通知其他子協程退出,這是會導致業務資料異常或者丟失的,那麼此刻我們就可以使用到 notify-and-wait 模式 來進行處理
我們就直接來寫一個主協程通知並等待多個子協程退出的 demo:
- 主協程呼叫 help 函式,得到一個 quit chan struct{} 型別的通道變數,主協程阻塞讀取 quit 的值
- help 函式根據傳入的引數 num 來建立 num 個子協程,並且使用 sync.WaitGroup 來控制
- 當主協程在 quit 通道中寫入資料時,主動通知所有子協程退出
- help 中的另外一個協程讀取到 quit 通道中的資料,便 close 掉 j 通道,觸發所有的子協程讀取 j 通道值的時候,得到的 ok 為 false,進而所有子協程退出
- wg.Wait() 等待所有子協程退出後,再在 quit 中寫入資料
- 主協程此時從 quit 中讀取到資料,則知道所有子協程全部退出,自己的主協程即刻退出
func fn(){
// 模擬在處理業務
time.Sleep(time.Second * 1)
}
func help(num int, f func()) chan struct{}{
quit := make(chan struct{})
j := make(chan int)
var wg sync.WaitGroup
// 建立子協程處理業務
for i:=0;i<num;i++{
wg.Add(1)
go func(){
defer wg.Done()
_,ok:=<-j
if !ok{
fmt.Println("exit child goroutine .")
return
}
// 子協程 正常執行業務
f()
}()
}
go func(){
<-quit
close(j)
// 等待子協程全部退出
wg.Wait()
quit <- struct{}{}
}()
return quit
}
func main(){
quit := help(10, fn)
// 模擬主程式處理在處理其他事項
// ...
time.Sleep(time.Second * 10)
quit <- struct{}{}
// 此處等待所有子程式退出
select{
case <- quit:
fmt.Println(" programs exit. ")
}
}
上述程式執行結果如下,可以看到 help 函式建立了 10 個子協程,主協程主動通知子協程全部退出,退出的時候也是 10 個子協程退出了,主協程才退出
上述程式,如果某一個子協程出現了問題,導致子協程不能完全退出,也就是說某些子協程在 f 函式中阻塞住了,那麼這個時候主協程豈不是一直無法退出???
那麼此時,在主協程通知子協程退出的時候,我們加上一個超時時間,表達意思為,超過某個時間,如果子協程還沒有全部退出完畢,那麼主協程仍然主動關閉程式,可以這樣寫:
- 設定一個定時器, 3 秒後會觸發,即可以從 t.C 中讀取到資料
t := time.NewTimer(time.Second * 3)
defer t.Stop()
// 此處等待所有子程式退出
select{
case <-t.C:
fmt.Println("timeout programs exit. ")
case <- quit:
fmt.Println(" 111 programs exit. ")
}
管道模式
說到管理,或許大家對 linux 裡面的管道更加熟悉吧,例如使用 linux 命令找到檔案中的 golang 這個字串
cat xxx.txt |grep "golang"
那麼對於 GO 語言併發模式中的管道模式也是類似的效果,我們就可以用這個管道模式來過濾資料
例如我們可以設計這樣一個程式,兄弟們可以動起手來寫一寫,評論區見哦:
- 整個程式總共使用 2 個通道
- help 函式中傳輸資料量 50 ,邏輯計算能夠被 5 整除的資料寫到第一個通道 ch1 中
- 另一個協程阻塞讀取 ch1 中的內容,並將取出的資料乘以 3 ,將結果寫入到 ch2 中
- 主協程就阻塞讀取 ch2 的內容,讀取到內容後,挨個列印出來
管道模式有兩種模式,扇出模式 和 扇入模式,這個比較好理解
- 扇出模式:多種型別的資料從同一個通道 channel 中讀取資料,直到通道關閉
- 扇入模式:輸入的時候有多個通道channel,程式將所有的通道內資料匯聚,統一輸入到另外一個通道channel A 裡面,另外一個程式則從這個通道channel A 中讀取資料,直到這個通道A關閉為止
超時模式和取消模式化
超時模式
上述例子中有專門說到如何去使用他,實際上我們還可以這樣用:
select{
case <- time.Afer(time.Second * 2):
fmt.Println("timeout programs exit. ")
case <- quit:
fmt.Println(" 111 programs exit. ")
}
取消模式
則是使用了 GO 語言的 context 包中的提供了上下文機制,可以在協程 goroutine 之間傳遞 deadline,取消等訊號
我們使用的時候例如可以這樣:
- 使用 context.WithCancel 建立一個可以被取消的上下文,啟動一個協程 在 3 秒後關閉上下文
- 使用 for 迴圈模擬處理業務,預設會走 select 的 default 分支
- 3 秒後 走到 select 的 ctx.Done(),則進入到了取消模式,程式退出
ctx, cancelFunc := context.WithCancel(context.Background())
go func() {
time.Sleep(time.Second * 3)
cancelFunc()
}()
for {
select {
case <-ctx.Done():
fmt.Println("program exit .")
return
default:
fmt.Println("I'm still here.")
time.Sleep(time.Second)
}
}
總的來說,今天分享了 GO 語言中常見的幾種併發模式:建立模式,退出模式,管道模式,超時模式和取消模式,更多的,還是要我們要思考其原理和應用起來,學習他們才能更加的有效
歡迎點贊,關注,收藏
朋友們,你的支援和鼓勵,是我堅持分享,提高質量的動力
好了,本次就到這裡
技術是開放的,我們的心態,更應是開放的。擁抱變化,向陽而生,努力向前行。
我是阿兵雲原生,歡迎點贊關注收藏,下次見~
本作品採用《CC 協議》,轉載必須註明作者和本文連結