Golang 并發(fā)控制模型的實現(xiàn)
Go語言的并發(fā)模型是CSP(通信順序進程),提倡通過通信來進行內(nèi)存共享,而不是通過共享內(nèi)存來實現(xiàn)通信。
控制并發(fā)有三種經(jīng)典的方式,使用 channel 通知實現(xiàn)并發(fā)控制、使用 sync 包中的 WaitGroup 實現(xiàn)并發(fā)控制、使用 Context 上下文實現(xiàn)并發(fā)控制。
一、使用 channel 通知實現(xiàn)并發(fā)控制
1、無緩沖通道
無緩沖通道,又叫做阻塞通道。發(fā)送方 (goroutine) 和接收方 (gouroutine) 必須是同步的,同時準備好,如果沒有同時準備好的話,一方就會一直阻塞住,直到另一方準備好為止。
使用無緩沖通道進行通信,將發(fā)送和接收的 goroutine 同步化,因此,無緩沖通道也被稱為同步通道。
ch := make(chan int) // 創(chuàng)建無緩沖通道
使用無緩沖通道實現(xiàn)并發(fā)控制:
package main import "fmt" func recv(c chan int) { fmt.Println("開始接收") ret := <-c fmt.Println("接收成功", ret) } func main() { ch := make(chan int) go recv(ch) // 啟用 goroutine 從通道接收值 ch <- 10 fmt.Println("發(fā)送成功") }
當子協(xié)程從無緩沖 channel 里接收值時,沒有發(fā)送方,子協(xié)程阻塞等待,直到主協(xié)程往無緩沖 channel 里發(fā)送值,子協(xié)程開始執(zhí)行,然后主協(xié)程開始執(zhí)行。
2、有緩沖通道
只要通道的容量大于0,那么該通道就是有緩沖通道,通道的容量表示通道中能最多存放元素的數(shù)量。發(fā)送方在緩沖區(qū)滿的時候阻塞,接收方不阻塞;接收方在緩沖區(qū)為空的時候阻塞,發(fā)送方不阻塞。
ch := make(chan int, 10) // 創(chuàng)建一個緩沖區(qū)為10的有緩沖通道 fmt.Println(len(ch)) // 通過len函數(shù)獲取當前通道內(nèi)元素數(shù)量 fmt.Println(cap(ch)) // 通過cap函數(shù)獲取通道的容量
使用緩沖區(qū)為 1 的通道實現(xiàn)并發(fā)控制:
package main import ( "fmt" "time" ) func recv(c chan int) { fmt.Println("開始接收") ret := <-c fmt.Println("接收成功", ret) } func main() { ch := make(chan int, 1) ch <- 10 go recv(ch) // 啟用 goroutine 從通道接收值 time.Sleep(time.Second) fmt.Println("發(fā)送成功") }
當主協(xié)程往緩沖區(qū)為1的 channel 里發(fā)送值時,不阻塞,子協(xié)程啟動,從無緩沖 channel 里接收值,主協(xié)程睡眠1秒,等待子協(xié)程執(zhí)行完,主協(xié)程在執(zhí)行。
二、使用 sync 包中的 WaitGroup 實現(xiàn)并發(fā)控制
1、sync.WaitGroup
在 sync 包中提供了 WaitGroup ,它會等待它收集的所有 goroutine 任務(wù)全部完成。
在主協(xié)程中調(diào)用 Add() 添加需要執(zhí)行 goroutine 的數(shù)量,在每一個 goroutine 執(zhí)行完成后調(diào)用 Done() ,表示這個 goroutine 已經(jīng)完成,主協(xié)程調(diào)用 Wait() 阻塞等待所有 goroutine 執(zhí)行完成,當所有的 goroutine 都執(zhí)行完成后,主協(xié)程返回。
實現(xiàn)原理:sync.WaitGroup 內(nèi)部維護著一個計數(shù)器,計數(shù)器的值可以增加和減少,當我們啟動 N 個并發(fā)任務(wù)時,將計數(shù)器增加 N,每個任務(wù)通過調(diào)用Done方法將計數(shù)器減1,通過調(diào)用Wait()來等待并發(fā)任務(wù)執(zhí)行完,當計數(shù)器的值為 0 時,表示所有并發(fā)任務(wù)都已經(jīng)完成。
sync.WaitGroup有以下三種方法:
- Add(N int) : 計數(shù)器 + N
- Done() : 計數(shù)器 - 1
- Wait() : 阻塞,直到計數(shù)器變?yōu)?
package main import ( "fmt" "sync" ) var wg sync.WaitGroup func hello() { defer wg.Done() fmt.Println("Hello Goroutine!") } func main() { wg.Add(3) go hello() // 啟動3個goroutine去執(zhí)行hello函數(shù) go hello() go hello() fmt.Println("main goroutine done!") wg.Wait() }
擴展:
在Golang官網(wǎng)中,有這么一句話:
A WaitGroup must not be copied after first use.
意思是,在 WaitGroup 第一次使用后,不能被拷貝。
為什么呢???
通過下面的例子我們淺淺分析一下。
package main import ( "fmt" "sync" ) func main() { wg := sync.WaitGroup{} for i := 0; i < 5; i++ { wg.Add(1) go func(wg sync.WaitGroup, i int) { fmt.Println(i) wg.Done() }(wg, i) } wg.Wait() }
提示所有的 goroutine 都已經(jīng)睡眠了,出現(xiàn)了死鎖。這是因為 wg 值拷貝傳遞到了子 goroutine 中,導致只有 Add 操作,Done 操作是在 wg 的副本執(zhí)行的, wg 的作用域為子協(xié)程,而不是全局,因此主協(xié)程就死鎖了。
改正方法:
- 指針,將匿名函數(shù)中 wg 的傳入類型改為 *sync.WaitGroup 。
func main() { wg := sync.WaitGroup{} for i := 0; i < 5; i++ { wg.Add(1) go func(wg *sync.WaitGroup, i int) { fmt.Println(i) wg.Done() }(&wg, i) } wg.Wait() }
- 閉包,將匿名函數(shù)中的 wg 的傳入?yún)?shù)去掉,在匿名函數(shù)中可以直接使用外面的 wg 變量。
func main() { wg := sync.WaitGroup{} for i := 0; i < 5; i++ { wg.Add(1) go func(i int) { fmt.Println(i) wg.Done() }(i) } wg.Wait() }
2、sync.Once
很多場景下,我們需要確保某些操作在高并發(fā)時只執(zhí)行一次,例如只加載一次配置文件。Go語言中的sync包提供了一個針對只執(zhí)行一次場景的解決方案 sync.Once。
sync.Once只有一個Do方法,Do(f func()) 。
實現(xiàn)原理:sync.Once 內(nèi)部包含一個互斥鎖和一個布爾值,互斥鎖保證布爾值和數(shù)據(jù)的安全,而布爾值用來記錄初始化是否完成,這樣設(shè)計就能保證初始化操作的時候是并發(fā)安全的,并且初始化操作也不會執(zhí)行多次。
樣例如下:延遲一個開銷很大的初始化操作到真正用到它的時候再執(zhí)行。
package main import ( "image" "sync" ) var icons map[string]image.Image var loadIconsOnce sync.Once func loadIcons() { // 加載圖片 icons = map[string]image.Image{} } // Icon 是并發(fā)安全的 func Icon(name string) image.Image { loadIconsOnce.Do(loadIcons) return icons[name] }
三、使用 Context 上下文實現(xiàn)并發(fā)控制
1、簡介
在一些簡單場景下使用 channel 和 WaitGroup 已經(jīng)足夠了,但是當面臨一些復(fù)雜多變的網(wǎng)絡(luò)并發(fā)場景下 channel 和 WaitGroup 顯得有些力不從心了。在并發(fā)程序中,由于超時、取消操作或者一些異常情況,往往需要進行搶占操作或者中斷后續(xù)操作。
舉個例子:在 Go http包的Server中,每一個請求在都有一個對應(yīng)的 goroutine 去處理。請求處理函數(shù)通常會啟動額外的 goroutine 用來訪問后端服務(wù),比如數(shù)據(jù)庫和RPC服務(wù),用來處理一個請求的 goroutine 通常需要訪問一些與請求特定的數(shù)據(jù),比如終端用戶的身份認證信息、驗證相關(guān)的token、請求的截止時間。 當一個請求被取消或超時時,所有用來處理該請求的 goroutine 都應(yīng)該迅速中斷退出,然后系統(tǒng)才能釋放這些 goroutine 占用的資源。
所以我們需要一種可以跟蹤 goroutine 的方案,才可以達到控制他們的目的,這就是Go語言為我們提供的 Context,稱之為上下文非常貼切,它就是 goroutine 的上下文。它包括一個程序的運行環(huán)境、現(xiàn)場和快照等。每個程序要運行時,都需要知道當前程序的運行狀態(tài),通常Go 將這些封裝在一個 Context 里,再將它傳給要執(zhí)行的 goroutine 。context 包主要是用來處理多個 goroutine 之間共享數(shù)據(jù),及多個 goroutine 的管理。
context常用的使用場景:
- 一個請求對應(yīng)多個goroutine之間的數(shù)據(jù)交互
- 超時控制
- 上下文控制
2、context 包
context 包的核心是 struct Context,接口聲明如下:
type Context interface { // 返回Context的超時時間(超時返回場景) Deadline() (deadline time.Time, ok bool) // 在Context超時或取消時(即結(jié)束了)返回一個關(guān)閉的channel,取消信號 // 如果當前Context超時或取消時,Done方法會返回一個channel,然后其他地方就可以通過判斷Done方法是否有返回(channel),如果有則說明Context已結(jié)束 // 故其可以作為廣播通知其他相關(guān)方本Context已結(jié)束,請做相關(guān)處理。 Done() <-chan struct{} // 返回Context取消的原因 Err() error // 返回Context相關(guān)數(shù)據(jù) Value(key any) any }
Context 對象是線程安全的(底層數(shù)據(jù)結(jié)構(gòu)加了互斥鎖),你可以把一個 Context 對象傳遞給任意個數(shù)的 gorotuine,對它執(zhí)行取消操作時,所有 goroutine 都會接收到取消信號。
一個 Context 不能擁有 Cancel 方法,同時我們也只能 Done channel 接收數(shù)據(jù)。原因是:接收取消信號的函數(shù)和發(fā)送信號的函數(shù)通常不是一個。一個典型的場景是:父操作為子操作操作啟動 goroutine,子操作也就不能取消父操作。
3、繼承 context
context 包提供了一些函數(shù),協(xié)助用戶從現(xiàn)有的 Context 對象創(chuàng)建新的 Context 對象。這些 Context 對象形成一棵樹:當一個 Context 對象被取消時,繼承自它的所有 Context 都會被取消。
Background 是所有 Context 對象樹的根,它不能被取消。context 包提供了三種context,分別是普通context、超時context、帶值的context:
func Background() Context { return backgroundCtx{} } // 普通context,通常這樣調(diào)用: ctx, cancel := context.WithCancel(context.Background()) func WithCancel(parent Context) (ctx Context, cancel CancelFunc) // 帶超時的context,超時之后會自動close對象的Done,與調(diào)用CancelFunc的效果一樣 // WithDeadline 明確地設(shè)置一個d指定的系統(tǒng)時鐘時間,如果超過就觸發(fā)超時 // WithTimeout 設(shè)置一個相對的超時時間,也就是deadline設(shè)為timeout加上當前的系統(tǒng)時間 // 因為兩者事實上都依賴于系統(tǒng)時鐘,所以可能存在微小的誤差,所以官方不推薦把超時間隔設(shè)置得太小 // 通常這樣調(diào)用: ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) // 帶有值的context,沒有CancelFunc,所以它只用于值的多goroutine傳遞和共享 // 通常這樣調(diào)用: ctx := context.WithValue(context.Background(), "key", myValue) func WithValue(parent Context, key, val interface{}) Context
WithCancel 和 WithTimeout 函數(shù)會返回繼承的 Context 對象, 這些對象可以比它們的父 Context 更早地取消。當請求處理函數(shù)返回時,與該請求關(guān)聯(lián)的 Context 會被取消。當使用多個副本發(fā)送請求時,可以使用 WithCancel 取消多余的請求。 WithTimeout 在設(shè)置對后端服務(wù)器請求超時時間時非常有用。WithValue 函數(shù)能夠?qū)⒄埱笞饔糜虻臄?shù)據(jù)與 Context 對象建立關(guān)系。
4、context 例子
下面的例子,主要描述的是通過一個 channel 實現(xiàn)一個為循環(huán)次數(shù)為5的循環(huán)。
package main import ( "context" "fmt" "time" ) func childFunc(cont context.Context, num *int) { ctx, _ := context.WithCancel(cont) for { select { case <-ctx.Done(): fmt.Println("child Done : ", ctx.Err()) return } } } func main() { gen := func(ctx context.Context) <-chan int { dst := make(chan int) n := 1 go func() { for { select { case <-ctx.Done(): fmt.Println("parent Done : ", ctx.Err()) return // returning not to leak the goroutine case dst <- n: n++ go childFunc(ctx, &n) } } }() return dst } ctx, cancel := context.WithCancel(context.Background()) for n := range gen(ctx) { fmt.Println(n) if n >= 5 { break } } cancel() time.Sleep(5 * time.Second) }
在每一個循環(huán)中產(chǎn)生一個goroutine,每一個goroutine中都傳入context,在每個goroutine中通過傳入 ctx 創(chuàng)建一個子Context,并且通過 select 一直監(jiān)控該Context的運行情況,當父 Context 退出的時候,代碼中并沒有明顯調(diào)用子 Context 的 Cancel 函數(shù),但是分析結(jié)果,子 Context 還是被正確合理的關(guān)閉了,這是因為,所有基于這個 Context 或者衍生的子 Context 都會收到通知,這時就可以進行清理操作了,最終釋放 goroutine,這就優(yōu)雅的解決了 goroutine 啟動后不可控的問題。
5、context 使用原則
- 不要把 context 放在結(jié)構(gòu)體中,要以參數(shù)的方式傳遞。
- 以 context 作為參數(shù)的函數(shù)方法,應(yīng)該把 context 作為第一個參數(shù),放在第一位。
- 給一個函數(shù)方法傳遞 context 的時候,不要傳遞nil,如果不知道傳遞什么,就使用context.TODO。
- context 的 Value 相關(guān)方法應(yīng)該傳遞必須的數(shù)據(jù),不要什么數(shù)據(jù)都使用這個傳遞。
- context 是線程安全的,底層數(shù)據(jù)結(jié)構(gòu)加了互斥鎖,可以放心的在多個goroutine中傳遞。
到此這篇關(guān)于Golang 并發(fā)控制模型的實現(xiàn)的文章就介紹到這了,更多相關(guān)Golang 并發(fā)控制內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
golang?recover函數(shù)使用中的一些坑解析
這篇文章主要為大家介紹了golang?recover函數(shù)使用中的一些坑解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-02-02Golang設(shè)計模式工廠模式實戰(zhàn)寫法示例詳解
這篇文章主要為大家介紹了Golang 工廠模式實戰(zhàn)寫法示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-08-08關(guān)于go平滑重啟庫overseer實現(xiàn)原理詳解
這篇文章主要為大家詳細介紹了關(guān)于go平滑重啟庫overseer實現(xiàn)原理,文中的示例代碼講解詳細,具有一定的參考價值,有需要的小伙伴可以參考下2023-11-11使用Go和Tesseract實現(xiàn)驗證碼識別的流程步驟
驗證碼主要用于區(qū)分人類用戶和機器程序,Tesseract 是一個開源的光學字符識別(OCR)引擎,支持多種語言和字體,并具有較高的識別準確率,它由 Google 維護,并且可以通過多種編程語言調(diào)用,本文給大家介紹了使用Go和Tesseract實現(xiàn)驗證碼識別的流程步驟2025-01-01golang實現(xiàn)通過smtp發(fā)送電子郵件的方法
這篇文章主要介紹了golang實現(xiàn)通過smtp發(fā)送電子郵件的方法,實例分析了Go語言基于SMTP協(xié)議發(fā)送郵件的相關(guān)技巧,需要的朋友可以參考下2016-07-07一文帶大家了解Go語言中的內(nèi)聯(lián)優(yōu)化
內(nèi)聯(lián)優(yōu)化是一種常見的編譯器優(yōu)化策略,通俗來講,就是把函數(shù)在它被調(diào)用的地方展開,這樣可以減少函數(shù)調(diào)用所帶來的開銷,本文主要為大家介紹了Go中內(nèi)聯(lián)優(yōu)化的具體使用,需要的可以參考下2023-05-05