Go語(yǔ)言中的并發(fā)模式你了解了嗎
工作中查看項(xiàng)目代碼,發(fā)現(xiàn)會(huì)存在使用 GO 語(yǔ)言做并發(fā)的時(shí)候出現(xiàn)各種各樣的異常情況,有的輸出結(jié)果和自己期望和設(shè)計(jì)的不一致,有的是程序直接阻塞住,更有甚者直接是程序 crash 掉。
實(shí)際上,出現(xiàn)上述的情況,還是因?yàn)槲覀儗?duì)于 GO 語(yǔ)言的并發(fā)模型和涉及的 GO 語(yǔ)言基礎(chǔ)不夠扎實(shí),誤解了語(yǔ)言的用法。
那么,對(duì)于 GO 語(yǔ)言的并發(fā)模式,我們一起來(lái)梳理一波。 GO 語(yǔ)言常見(jiàn)的并發(fā)模式有這些:
- 創(chuàng)建模式
- 退出模式
- 管道模式
- 超時(shí)模式和取消模式
在 GO 語(yǔ)言里面,咱們使用使用并發(fā),自然離不開(kāi)使用 GO 語(yǔ)言的協(xié)程 goroutine,通道 channel 和 多路復(fù)用 select,接下來(lái)就來(lái)看看各種模式都是如何去搭配使用這三個(gè)關(guān)鍵原語(yǔ)的
創(chuàng)建模式
使用過(guò)通道和協(xié)程的朋友對(duì)于創(chuàng)建模式肯定不會(huì)模式,這是一個(gè)非常常用的方式,也是一個(gè)非常簡(jiǎn)單的使用方式:
- 主協(xié)程中調(diào)用 help 函數(shù),返回一個(gè)通道 ch 變量
- 通道 ch 用于主協(xié)程和 子協(xié)程之間的通信,其中通道的數(shù)據(jù)類(lèi)型完全可以自行定義
type XXX struct{...} func help(fn func()) chan XXX { ch := make(chan XXX) // 開(kāi)啟一個(gè)協(xié)程 go func(){ // 此處的協(xié)程可以控制和外部的 主協(xié)程 通過(guò) ch 來(lái)進(jìn)行通信,達(dá)到一定邏輯便可以執(zhí)行自己的 fn 函數(shù) fn() ch <- XXX }() } func main(){ ch := help(func(){ fmt.Println("這是GO 語(yǔ)言 并發(fā)模式之 創(chuàng)建模式") }) <- ch }
退出模式
程序的退出我們應(yīng)該也不會(huì)陌生,對(duì)于一些常駐的服務(wù),如果是要退出程序,自然是不能直接就斷掉,此時(shí)會(huì)有一些連接和業(yè)務(wù)并沒(méi)有關(guān)閉,直接關(guān)閉程序會(huì)導(dǎo)致業(yè)務(wù)異常,例如在關(guān)閉過(guò)程中最后一個(gè) http 請(qǐng)求沒(méi)有正常響應(yīng)等等等
此時(shí),就需要做優(yōu)雅關(guān)閉了,對(duì)于協(xié)程 goroutine 退出有 3 種模式
- 分離模式
- join 模式
- notify-and-wait 模式
分離模式
此處的分離模式,分離這個(gè)術(shù)語(yǔ)實(shí)際上是線程中的術(shù)語(yǔ),pthread detached
分離模式可以理解為,咱們創(chuàng)建的協(xié)程 goroutine,直接分離,創(chuàng)建子協(xié)程的父協(xié)程不用關(guān)心子協(xié)程是如何退出的,子協(xié)程的生命周期主要與它執(zhí)行的主函數(shù)有關(guān),咱們 return 之后,子協(xié)程也就結(jié)束了
對(duì)于這類(lèi)分離模式的協(xié)程,咱們需要關(guān)注兩類(lèi),一種是一次性的任務(wù),咱們 go 出來(lái)后,執(zhí)行簡(jiǎn)單任務(wù)完畢后直接退出,一種是常駐程序,需要優(yōu)雅退出,處理一些垃圾回收的事情
例如這樣:
- 主程序中設(shè)置一個(gè)通道變量 ch ,類(lèi)型為 os.Signal
- 然后主程序就開(kāi)始各種創(chuàng)建協(xié)程執(zhí)行自己的各種業(yè)務(wù)
- 直到程序收到了 syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT 任意一個(gè)信號(hào)的時(shí)候,則會(huì)開(kāi)始進(jìn)行垃圾回收等清理工作,執(zhí)行完畢后,程序再進(jìn)行退出
func main(){ ch := make(chan os.Signal) signal.Notify(c, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) // ... // go 程序執(zhí)行其他業(yè)務(wù) // ... for i := range ch { switch i { case syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT: // 做一些清理工作 os.Exit(0) } } }
join 模式
看到這個(gè)關(guān)鍵字,是不是也似曾相識(shí),和線程貌似很像,例如 線程中 父線程可以通過(guò) pthread_join 來(lái)等待子線程結(jié)束,并且還可以獲取子線程的結(jié)束狀態(tài)
GO 語(yǔ)言中等待子協(xié)程退出并且獲取子協(xié)程的退出狀態(tài),咱們就可以使用通道 channel 的方式來(lái)進(jìn)行處理
例子1
等待一個(gè)子協(xié)程退出,并獲取退出狀態(tài)
- 主協(xié)程中調(diào)用 help 方法得到一個(gè) ch 通道變量,主協(xié)程阻塞著讀 ch
- help 中開(kāi)辟一個(gè)子協(xié)程去執(zhí)行傳入的 fn 回調(diào)函數(shù),并傳參為 ok bool
- 實(shí)際 fn 函數(shù)判斷傳參 ok 是否是 true,若不是則返回具體的錯(cuò)誤信息,若是 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) }
看上如上程序,我們就可以知道,第一次調(diào)用 help(fn , true)
,主協(xié)程等待子協(xié)程退出的時(shí)候,會(huì)得到一個(gè)錯(cuò)誤信息,為 not ok ...
, 第二次調(diào)用 help(fn , false)
的時(shí)候,返回的 err 是一個(gè) nil
通過(guò)上述這種方式,主協(xié)程不僅可以輕易的等待一個(gè)子協(xié)程退出,還可以獲取到子協(xié)程退出的狀態(tài)
那么,主協(xié)程如果是等待多個(gè)協(xié)程退出呢?需要如何處理?
例子2
主協(xié)程等待多個(gè)協(xié)程退出咱們就需要使用到 GO 中的 sync.WaitGroup
- 使用 help 函數(shù),傳入回調(diào)函數(shù),參數(shù)1 bool,參數(shù)2 int ,其中參數(shù) 2 表示開(kāi)辟子協(xié)程的個(gè)數(shù),返回值為一個(gè)無(wú)緩沖的 channel 變量,數(shù)據(jù)類(lèi)型是 struct{}
- 使用 var wg sync.WaitGroup ,開(kāi)辟子協(xié)程的時(shí)候記錄一次 wg.Add(1),當(dāng)子協(xié)程退出時(shí) ,記錄退出 wg.Done()
- help 中再另起一個(gè)協(xié)程 wg.Wait() 等待所有子協(xié)程退出,并將 ch 變量寫(xiě)入值
- 主協(xié)程阻塞讀取 ch 變量的值,待所有子協(xié)程都退出之后,help 中寫(xiě)入到 ch 中的數(shù)據(jù),主協(xié)程就能馬上收到 ch 中的數(shù)據(jù),并退出程序
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(){ // 等待所有子協(xié)程退出 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 模式
可以看到上述模式,都是主協(xié)程等待一個(gè)子協(xié)程,或者多個(gè)子協(xié)程結(jié)束后,主協(xié)程再進(jìn)行退出,或者處理完垃圾回收后退出
那么如果主協(xié)程要主動(dòng)通知子協(xié)程退出,我們應(yīng)該要如何處理呢?
同樣的問(wèn)題,如果主協(xié)程自己退出了,而沒(méi)有通知其他子協(xié)程退出,這是會(huì)導(dǎo)致業(yè)務(wù)數(shù)據(jù)異?;蛘邅G失的,那么此刻我們就可以使用到 notify-and-wait 模式 來(lái)進(jìn)行處理
我們就直接來(lái)寫(xiě)一個(gè)主協(xié)程通知并等待多個(gè)子協(xié)程退出的 demo:
- 主協(xié)程調(diào)用 help 函數(shù),得到一個(gè) quit chan struct{} 類(lèi)型的通道變量,主協(xié)程阻塞讀取 quit 的值
- help 函數(shù)根據(jù)傳入的參數(shù) num 來(lái)創(chuàng)建 num 個(gè)子協(xié)程,并且使用 sync.WaitGroup 來(lái)控制
- 當(dāng)主協(xié)程在 quit 通道中寫(xiě)入數(shù)據(jù)時(shí),主動(dòng)通知所有子協(xié)程退出
- help 中的另外一個(gè)協(xié)程讀取到 quit 通道中的數(shù)據(jù),便 close 掉 j 通道,觸發(fā)所有的子協(xié)程讀取 j 通道值的時(shí)候,得到的 ok 為 false,進(jìn)而所有子協(xié)程退出
- wg.Wait() 等待所有子協(xié)程退出后,再在 quit 中寫(xiě)入數(shù)據(jù)
- 主協(xié)程此時(shí)從 quit 中讀取到數(shù)據(jù),則知道所有子協(xié)程全部退出,自己的主協(xié)程即刻退出
func fn(){ // 模擬在處理業(yè)務(wù) 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 // 創(chuàng)建子協(xié)程處理業(yè)務(wù) for i:=0;i<num;i++{ wg.Add(1) go func(){ defer wg.Done() _,ok:=<-j if !ok{ fmt.Println("exit child goroutine .") return } // 子協(xié)程 正常執(zhí)行業(yè)務(wù) f() }() } go func(){ <-quit close(j) // 等待子協(xié)程全部退出 wg.Wait() quit <- struct{}{} }() return quit } func main(){ quit := help(10, fn) // 模擬主程序處理在處理其他事項(xiàng) // ... time.Sleep(time.Second * 10) quit <- struct{}{} // 此處等待所有子程序退出 select{ case <- quit: fmt.Println(" programs exit. ") } }
上述程序執(zhí)行結(jié)果如下,可以看到 help 函數(shù)創(chuàng)建了 10 個(gè)子協(xié)程,主協(xié)程主動(dòng)通知子協(xié)程全部退出,退出的時(shí)候也是 10 個(gè)子協(xié)程退出了,主協(xié)程才退出
上述程序,如果某一個(gè)子協(xié)程出現(xiàn)了問(wèn)題,導(dǎo)致子協(xié)程不能完全退出,也就是說(shuō)某些子協(xié)程在 f 函數(shù)中阻塞住了,那么這個(gè)時(shí)候主協(xié)程豈不是一直無(wú)法退出???
那么此時(shí),在主協(xié)程通知子協(xié)程退出的時(shí)候,我們加上一個(gè)超時(shí)時(shí)間,表達(dá)意思為,超過(guò)某個(gè)時(shí)間,如果子協(xié)程還沒(méi)有全部退出完畢,那么主協(xié)程仍然主動(dòng)關(guān)閉程序,可以這樣寫(xiě):
設(shè)定一個(gè)定時(shí)器, 3 秒后會(huì)觸發(fā),即可以從 t.C 中讀取到數(shù)據(jù)
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. ") }
管道模式
說(shuō)到管理,或許大家對(duì) linux 里面的管道更加熟悉吧,例如使用 linux 命令找到文件中的 golang 這個(gè)字符串
cat xxx.txt |grep "golang"
那么對(duì)于 GO 語(yǔ)言并發(fā)模式中的管道模式也是類(lèi)似的效果,我們就可以用這個(gè)管道模式來(lái)過(guò)濾數(shù)據(jù)
例如我們可以設(shè)計(jì)這樣一個(gè)程序,兄弟們可以動(dòng)起手來(lái)寫(xiě)一寫(xiě),評(píng)論區(qū)見(jiàn)哦:
- 整個(gè)程序總共使用 2 個(gè)通道
- help 函數(shù)中傳輸數(shù)據(jù)量 50 ,邏輯計(jì)算能夠被 5 整除的數(shù)據(jù)寫(xiě)到第一個(gè)通道 ch1 中
- 另一個(gè)協(xié)程阻塞讀取 ch1 中的內(nèi)容,并將取出的數(shù)據(jù)乘以 3 ,將結(jié)果寫(xiě)入到 ch2 中
- 主協(xié)程就阻塞讀取 ch2 的內(nèi)容,讀取到內(nèi)容后,挨個(gè)打印出來(lái)
管道模式有兩種模式,扇出模式 和 扇入模式,這個(gè)比較好理解
- 扇出模式:多種類(lèi)型的數(shù)據(jù)從同一個(gè)通道 channel 中讀取數(shù)據(jù),直到通道關(guān)閉
- 扇入模式:輸入的時(shí)候有多個(gè)通道channel,程序?qū)⑺械耐ǖ纼?nèi)數(shù)據(jù)匯聚,統(tǒng)一輸入到另外一個(gè)通道channel A 里面,另外一個(gè)程序則從這個(gè)通道channel A 中讀取數(shù)據(jù),直到這個(gè)通道A關(guān)閉為止
超時(shí)模式和取消模式化
超時(shí)模式
上述例子中有專(zhuān)門(mén)說(shuō)到如何去使用他,實(shí)際上我們還可以這樣用:
select{ case <- time.Afer(time.Second * 2): fmt.Println("timeout programs exit. ") case <- quit: fmt.Println(" 111 programs exit. ") }
取消模式
則是使用了 GO 語(yǔ)言的 context 包中的提供了上下文機(jī)制,可以在協(xié)程 goroutine 之間傳遞 deadline,取消等信號(hào)
我們使用的時(shí)候例如可以這樣:
- 使用 context.WithCancel 創(chuàng)建一個(gè)可以被取消的上下文,啟動(dòng)一個(gè)協(xié)程 在 3 秒后關(guān)閉上下文
- 使用 for 循環(huán)模擬處理業(yè)務(wù),默認(rèn)會(huì)走 select 的 default 分支
- 3 秒后 走到 select 的 ctx.Done(),則進(jìn)入到了取消模式,程序退出
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) } }
總的來(lái)說(shuō),今天分享了 GO 語(yǔ)言中常見(jiàn)的幾種并發(fā)模式:創(chuàng)建模式,退出模式,管道模式,超時(shí)模式和取消模式,更多的,還是要我們要思考其原理和應(yīng)用起來(lái),學(xué)習(xí)他們才能更加的有效
以上就是Go語(yǔ)言中的并發(fā)模式你了解了嗎的詳細(xì)內(nèi)容,更多關(guān)于go并發(fā)模式的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Golang中自定義json序列化時(shí)間格式的示例代碼
Go語(yǔ)言作為一個(gè)由Google開(kāi)發(fā),號(hào)稱(chēng)互聯(lián)網(wǎng)的C語(yǔ)言的語(yǔ)言,自然也對(duì)JSON格式支持很好,下面這篇文章主要介紹了關(guān)于Golang中自定義json序列化時(shí)間格式的相關(guān)內(nèi)容,下面話不多說(shuō)了,來(lái)一起看看詳細(xì)的介紹吧2024-08-08基于gin的golang web開(kāi)發(fā)之認(rèn)證利器jwt
這篇文章主要介紹了基于gin的golang web開(kāi)發(fā)之認(rèn)證利器jwt,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-12-12Hugo 游樂(lè)場(chǎng)內(nèi)容初始化示例詳解
這篇文章主要為大家介紹了Hugo 游樂(lè)場(chǎng)內(nèi)容初始化示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02