Golang并發(fā)控制的三種實(shí)現(xiàn)方法
我們考慮這么一種場(chǎng)景,協(xié)程A執(zhí)行過(guò)程中需要?jiǎng)?chuàng)建子協(xié)程A1、A2、A3…An,協(xié)程A創(chuàng)建完子協(xié)程后就等待子協(xié)程退 出。針對(duì)這種場(chǎng)景,GO提供了三種解決方案:
Channel: 使用channel控制子協(xié)程
WaitGroup : 使用信號(hào)量機(jī)制控制子協(xié)程
Context: 使用上下文控制子協(xié)程
三種方案各有優(yōu)劣,比如Channel優(yōu)點(diǎn)是實(shí)現(xiàn)簡(jiǎn)單,清晰易懂,WaitGroup優(yōu)點(diǎn)是子協(xié)程個(gè)數(shù)動(dòng)態(tài)可調(diào)整,Context 優(yōu)點(diǎn)是對(duì)子協(xié)程派生出來(lái)的孫子協(xié)程的控制。缺點(diǎn)是相對(duì)而言的,要結(jié)合實(shí)例應(yīng)用場(chǎng)景進(jìn)行選擇。
channel
channel一般用于協(xié)程之間的通信,channel也可以用于并發(fā)控制。比如主協(xié)程啟動(dòng)N個(gè)子協(xié)程,主協(xié)程等待所有子 協(xié)程退出后再繼續(xù)后續(xù)流程,這種場(chǎng)景下channel也可輕易實(shí)現(xiàn)。
場(chǎng)景示例
下面程序展示一個(gè)使用channel控制子協(xié)程的例子:
上面程序通過(guò)創(chuàng)建N個(gè)channel來(lái)管理N個(gè)協(xié)程,每個(gè)協(xié)程都有一個(gè)channel用于跟父協(xié)程通信,父協(xié)程創(chuàng)建完所有協(xié) 程中等待所有協(xié)程結(jié)束。這個(gè)例子中,父協(xié)程僅僅是等待子協(xié)程結(jié)束,其實(shí)父協(xié)程也可以向管道中寫(xiě)入數(shù)據(jù)通知子協(xié)程結(jié)束,這時(shí)子協(xié)程需要 定期的探測(cè)管道中是否有消息出現(xiàn)。
總結(jié)
使用channel來(lái)控制子協(xié)程的優(yōu)點(diǎn)是實(shí)現(xiàn)簡(jiǎn)單,缺點(diǎn)是當(dāng)需要大量創(chuàng)建協(xié)程時(shí)就需要有相同數(shù)量的channel,而且對(duì)于子協(xié)程繼續(xù)派生出來(lái)的協(xié)程不方便控制。后面繼續(xù)介紹的WaitGroup、Context看起來(lái)比channel優(yōu)雅一些,在各種開(kāi)源組件中使用頻率比channel高得 多。
WaitGroup
WaitGroup是Golang應(yīng)用開(kāi)發(fā)過(guò)程中經(jīng)常使用的并發(fā)控制技術(shù)。 WaitGroup,可理解為Wait-Goroutine-Group,即等待一組goroutine結(jié)束。比如某個(gè)goroutine需要等待其 他幾個(gè)goroutine全部完成,那么使用WaitGroup可以輕松實(shí)現(xiàn)。
下面程序展示了一個(gè)goroutine等待另外兩個(gè)goroutine結(jié)束的例子:
簡(jiǎn)單的說(shuō),上面程序中wg內(nèi)部維護(hù)了一個(gè)計(jì)數(shù)器:
1.啟動(dòng)goroutine前將計(jì)數(shù)器通過(guò)Add(2)將計(jì)數(shù)器設(shè)置為待啟動(dòng)的goroutine個(gè)數(shù)。
2. 啟動(dòng)goroutine后,使用Wait()方法阻塞自己,等待計(jì)數(shù)器變?yōu)?。
3. 每個(gè)goroutine執(zhí)行結(jié)束通過(guò)Done()方法將計(jì)數(shù)器減1。
4. 計(jì)數(shù)器變?yōu)?后,阻塞的goroutine被喚醒。
其實(shí)WaitGroup也可以實(shí)現(xiàn)一組goroutine等待另一組goroutine,這有點(diǎn)像玩雜技,很容出錯(cuò),如果不了解其實(shí)現(xiàn)原理更是如此。實(shí)際上,WaitGroup的實(shí)現(xiàn)源碼非常簡(jiǎn)單。
信號(hào)量
信號(hào)量是Unix系統(tǒng)提供的一種保護(hù)共享資源的機(jī)制,用于防止多個(gè)線程同時(shí)訪問(wèn)某個(gè)資源。 可簡(jiǎn)單理解為信號(hào)量為一個(gè)數(shù)值:
當(dāng)信號(hào)量>0時(shí),表示資源可用,獲取信號(hào)量時(shí)系統(tǒng)自動(dòng)將信號(hào)量減1;
當(dāng)信號(hào)量= =0時(shí),表示資源暫不可用,獲取信號(hào)量時(shí),當(dāng)前線程會(huì)進(jìn)入睡眠,當(dāng)信號(hào)量為正時(shí)被喚醒;
由于WaitGroup實(shí)現(xiàn)中也使用了信號(hào)量,在此做個(gè)簡(jiǎn)單介紹。
WaitGroup數(shù)據(jù)結(jié)構(gòu)
源碼包中 src/sync/waitgroup.go:WaitGroup 定義了其數(shù)據(jù)結(jié)構(gòu):
type WaitGroup struct { state1 [3]uint32 }
state1是個(gè)長(zhǎng)度為3的數(shù)組,其中包含了state和一個(gè)信號(hào)量,而state實(shí)際上是兩個(gè)計(jì)數(shù)器:
- counter: 當(dāng)前還未執(zhí)行結(jié)束的goroutine計(jì)數(shù)器
- waiter count: 等待goroutine-group結(jié)束的goroutine數(shù)量,即有多少個(gè)等候者 semaphore: 信號(hào)量
考慮到字節(jié)是否對(duì)齊,三者出現(xiàn)的位置不同,為簡(jiǎn)單起見(jiàn),依照字節(jié)已對(duì)齊情況下,三者在內(nèi)存中的位置如下所示:
WaitGroup對(duì)外提供三個(gè)接口:
Add(delta int): 將delta值加到counter中
Wait(): waiter遞增1,并阻塞等待信號(hào)量
semaphore Done(): counter遞減1,按照waiter數(shù)值釋放相應(yīng)次數(shù)信號(hào)量
下面分別介紹這三個(gè)函數(shù)的實(shí)現(xiàn)細(xì)節(jié)。
Add(delta int)
Add()做了兩件事,一是把delta值累加到counter中,因?yàn)閐elta可以為負(fù)值,也就是說(shuō)counter有可能變成0或 負(fù)值,所以第二件事就是當(dāng)counter值變?yōu)?時(shí),跟據(jù)waiter數(shù)值釋放等量的信號(hào)量,把等待的goroutine全部喚 醒,如果counter變?yōu)樨?fù)值,則panic
Add()偽代碼如下:
Wait()
Wait()方法也做了兩件事,一是累加waiter, 二是阻塞等待信號(hào)量
這里用到了CAS算法保證有多個(gè)goroutine同時(shí)執(zhí)行Wait()時(shí)也能正確累加waiter。
Done()
Done()只做一件事,即把counter減1,我們知道Add()可以接受負(fù)值,所以Done實(shí)際上只是調(diào)用了Add(-1)。 源碼如下:
func (wg *WaitGroup) Done() { wg.Add(-1) }
Done()的執(zhí)行邏輯就轉(zhuǎn)到了Add(),實(shí)際上也正是最后一個(gè)完成的goroutine把等待者喚醒的。
注意事項(xiàng):Add()操作必須早于Wait(), 否則會(huì)panicAdd()設(shè)置的值必須與實(shí)際等待的goroutine個(gè)數(shù)一致,否則會(huì)panic
context
Golang context是Golang應(yīng)用開(kāi)發(fā)常用的并發(fā)控制技術(shù),它與WaitGroup最大的不同點(diǎn)是context對(duì)于派生 goroutine有更強(qiáng)的控制力,它可以控制多級(jí)的goroutine。
context翻譯成中文是”上下文”,即它可以控制一組呈樹(shù)狀結(jié)構(gòu)的goroutine,每個(gè)goroutine擁有相同的上下 文。
典型的使用場(chǎng)景如下圖所示:
上圖中由于goroutine派生出子goroutine,而子goroutine又繼續(xù)派生新的goroutine,這種情況下使用 WaitGroup就不太容易,因?yàn)樽觛oroutine個(gè)數(shù)不容易確定。而使用context就可以很容易實(shí)現(xiàn)。
Context實(shí)現(xiàn)原理
context實(shí)際上只定義了接口,凡是實(shí)現(xiàn)該接口的類(lèi)都可稱(chēng)為是一種context,官方包中實(shí)現(xiàn)了幾個(gè)常用的 context,分別可用于不同的場(chǎng)景。
接口定義
源碼包中 src/context/context.go:Context 定義了該接口:
基礎(chǔ)的context接口只定義了4個(gè)方法,下面分別簡(jiǎn)要說(shuō)明一下:
Deadline()
該方法返回一個(gè)deadline和標(biāo)識(shí)是否已設(shè)置deadline的bool值,如果沒(méi)有設(shè)置deadline,則ok == false,此 時(shí)deadline為一個(gè)初始值的time.Time值
Done()
該方法返回一個(gè)channel,需要在select-case語(yǔ)句中使用,如”case <-context.Done():”。
當(dāng)context關(guān)閉后,Done()返回一個(gè)被關(guān)閉的管道,關(guān)閉的管理仍然是可讀的,據(jù)此goroutine可以收到關(guān)閉請(qǐng) 求;當(dāng)context還未關(guān)閉時(shí),Done()返回nil。
Err()
該方法描述context關(guān)閉的原因。關(guān)閉原因由context實(shí)現(xiàn)控制,不需要用戶設(shè)置。比如Deadline context,關(guān) 閉原因可能是因?yàn)閐eadline,也可能提前被主動(dòng)關(guān)閉,那么關(guān)閉原因就會(huì)不同:
因deadline關(guān)閉:“context deadline exceeded”;
因主動(dòng)關(guān)閉: “context canceled”。
當(dāng)context關(guān)閉后,Err()返回context的關(guān)閉原因;當(dāng)context還未關(guān)閉時(shí),Err()返回nil;
Value()
有一種context,它不是用于控制呈樹(shù)狀分布的goroutine,而是用于在樹(shù)狀分布的goroutine間傳遞信息。
Value()方法就是用于此種類(lèi)型的context,該方法根據(jù)key值查詢(xún)map中的value。具體使用后面示例說(shuō)明。
空context
context包中定義了一個(gè)空的context, 名為emptyCtx,用于context的根節(jié)點(diǎn),空的context只是簡(jiǎn)單的實(shí)現(xiàn) 了Context,本身不包含任何值,僅用于其他context的父節(jié)點(diǎn)。
emptyCtx類(lèi)型定義如下代碼所示:
context包中定義了一個(gè)公用的emptCtx全局變量,名為background,可以使用context.Background()獲取 它,實(shí)現(xiàn)代碼如下所示:
context包提供了4個(gè)方法創(chuàng)建不同類(lèi)型的context,使用這四個(gè)方法時(shí)如果沒(méi)有父context,都需要傳入 backgroud,即backgroud作為其父節(jié)點(diǎn):
WithCancel()
WithDeadline()
WithTimeout()
WithValue()
context包中實(shí)現(xiàn)Context接口的struct,除了emptyCtx外,還有cancelCtx、timerCtx和valueCtx三種,正 是基于這三種context實(shí)例,實(shí)現(xiàn)了上述4種類(lèi)型的context。
context包中各context類(lèi)型之間的關(guān)系,如下圖所示:
struct cancelCtx、valueCtx、valueCtx都繼承于Context,下面分別介紹這三個(gè)struct。
cancelCtx
源碼包中 src/context/context.go:cancelCtx 定義了該類(lèi)型context:
children中記錄了由此context派生的所有child,此context被cancle時(shí)會(huì)把其中的所有child都cancle掉。
cancelCtx與deadline和value無(wú)關(guān),所以只需要實(shí)現(xiàn)Done()和Err()接口外露接口即可。
Done()接口實(shí)現(xiàn)
按照Context定義,Done()接口只需要返回一個(gè)channel即可,對(duì)于cancelCtx來(lái)說(shuō)只需要返回成員變量done即 可。
這里直接看下源碼,非常簡(jiǎn)單:
由于cancelCtx沒(méi)有指定初始化函數(shù),所以cancelCtx.done可能還未分配,所以需要考慮初始化。 cancelCtx.done會(huì)在context被cancel時(shí)關(guān)閉,所以cancelCtx.done的值一般經(jīng)歷如三個(gè)階段:nil —> chan struct{} —> closed chan。
Err()接口實(shí)現(xiàn)
按照Context定義,Err()只需要返回一個(gè)error告知context被關(guān)閉的原因。對(duì)于cancelCtx來(lái)說(shuō)只需要返回成員 變量err即可。還是直接看下源碼:
cancelCtx.err默認(rèn)是nil,在context被cancel時(shí)指定一個(gè)error變量: var Canceled = errors.New(“context canceled”) 。
cancel()接口實(shí)現(xiàn)
cancel()內(nèi)部方法是理解cancelCtx的最關(guān)鍵的方法,其作用是關(guān)閉自己和其后代,其后代存儲(chǔ)在 cancelCtx.children的map中,其中key值即后代對(duì)象,value值并沒(méi)有意義,這里使用map只是為了方便查詢(xún)而 已。
cancel方法實(shí)現(xiàn)偽代碼如下所示:
實(shí)際上,WithCancel()返回的第二個(gè)用于cancel context的方法正是此cancel()。
WithCancel()方法實(shí)現(xiàn)
WithCancel()方法作了三件事:
- 初始化一個(gè)cancelCtx實(shí)例
- 將cancelCtx實(shí)例添加到其父節(jié)點(diǎn)的children中(如果父節(jié)點(diǎn)也可以被cancel的話)
- 返回cancelCtx實(shí)例和cancel()方法
其實(shí)現(xiàn)源碼如下所示:
這里將自身添加到父節(jié)點(diǎn)的過(guò)程有必要簡(jiǎn)單說(shuō)明一下:
- 如果父節(jié)點(diǎn)也支持cancel,也就是說(shuō)其父節(jié)點(diǎn)肯定有children成員,那么把新context添加到children里 即可;
- 如果父節(jié)點(diǎn)不支持cancel,就繼續(xù)向上查詢(xún),直到找到一個(gè)支持cancel的節(jié)點(diǎn),把新context添加到 children里;
- 如果所有的父節(jié)點(diǎn)均不支持cancel,則啟動(dòng)一個(gè)協(xié)程等待父節(jié)點(diǎn)結(jié)束,然后再把當(dāng)前context結(jié)束。
典型使用案例
一個(gè)典型的使用cancel context的例子如下所示:
上面代碼中協(xié)程HandelRequest()用于處理某個(gè)請(qǐng)求,其又會(huì)創(chuàng)建兩個(gè)協(xié)程:WriteRedis()、 WriteDatabase(),main協(xié)程創(chuàng)建創(chuàng)建context,并把context在各子協(xié)程間傳遞,main協(xié)程在適當(dāng)?shù)臅r(shí)機(jī)可以 cancel掉所有子協(xié)程。
程序輸出如下所示:
timerCtx
源碼包中 src/context/context.go:timerCtx 定義了該類(lèi)型context:
type timerCtx struct { cancelCtx timer *time.Timer // Under cancelCtx.mu. deadline time.Time }
timerCtx在cancelCtx基礎(chǔ)上增加了deadline用于標(biāo)示自動(dòng)cancel的最終時(shí)間,而timer就是一個(gè)觸發(fā)自動(dòng) cancel的定時(shí)器。由此,衍生出WithDeadline()和WithTimeout()。實(shí)現(xiàn)上這兩種類(lèi)型實(shí)現(xiàn)原理一樣,只不過(guò)使用語(yǔ)境不一樣:deadline: 指定最后期限,比如context將2018.10.20 00:00:00之時(shí)自動(dòng)結(jié)束 timeout: 指定最長(zhǎng)存活時(shí)間,比如context將在30s后結(jié)束。對(duì)于接口來(lái)說(shuō),timerCtx在cancelCtx基礎(chǔ)上還需要實(shí)現(xiàn)Deadline()和cancel()方法,其中cancel()方法是重 寫(xiě)的。
Deadline()接口實(shí)現(xiàn)
Deadline()方法僅僅是返回timerCtx.deadline而矣。而timerCtx.deadline是WithDeadline()或 WithTimeout()方法設(shè)置的。
cancel()接口實(shí)現(xiàn)
cancel()方法基本繼承cancelCtx,只需要額外把timer關(guān)閉。
timerCtx被關(guān)閉后,timerCtx.cancelCtx.err將會(huì)存儲(chǔ)關(guān)閉原因:
如果deadline到來(lái)之前手動(dòng)關(guān)閉,則關(guān)閉原因與cancelCtx顯示一致;
如果deadline到來(lái)時(shí)自動(dòng)關(guān)閉,則原因?yàn)椋?rdquo;context deadline exceeded”
WithDeadline()方法實(shí)現(xiàn)
WithDeadline()方法實(shí)現(xiàn)步驟如下:
初始化一個(gè)timerCtx實(shí)例
將timerCtx實(shí)例添加到其父節(jié)點(diǎn)的children中(如果父節(jié)點(diǎn)也可以被cancel的話)
啟動(dòng)定時(shí)器,定時(shí)器到期后會(huì)自動(dòng)cancel本context
返回timerCtx實(shí)例和cancel()方法
也就是說(shuō),timerCtx類(lèi)型的context不僅支持手動(dòng)cancel,也會(huì)在定時(shí)器到來(lái)后自動(dòng)cancel。
WithTimeout()方法實(shí)現(xiàn)
WithTimeout()實(shí)際調(diào)用了WithDeadline,二者實(shí)現(xiàn)原理一致。 看代碼會(huì)非常清晰:
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { return WithDeadline(parent, time.Now().Add(timeout)) }
典型使用案例下面例子中使用WithTimeout()獲得一個(gè)context并在其了協(xié)程中傳遞:
主協(xié)程中創(chuàng)建一個(gè)10s超時(shí)的context,并將其傳遞給子協(xié)程,10s自動(dòng)關(guān)閉context。程序輸出如下:
valueCtx
源碼包中 src/context/context.go:valueCtx 定義了該類(lèi)型context:
type valueCtx struct { Context key, val interface{} }
valueCtx只是在Context基礎(chǔ)上增加了一個(gè)key-value對(duì),用于在各級(jí)協(xié)程間傳遞一些數(shù)據(jù)。由于valueCtx既不需要cancel,也不需要deadline,那么只需要實(shí)現(xiàn)Value()接口即可。
Value()接口實(shí)現(xiàn)
由valueCtx數(shù)據(jù)結(jié)構(gòu)定義可見(jiàn),valueCtx.key和valueCtx.val分別代表其key和value值。 實(shí)現(xiàn)也很簡(jiǎn)單:
這里有個(gè)細(xì)節(jié)需要關(guān)注一下,即當(dāng)前context查找不到key時(shí),會(huì)向父節(jié)點(diǎn)查找,如果查詢(xún)不到則最終返回interface{}。也就是說(shuō),可以通過(guò)子context查詢(xún)到父的value值。
WithValue()方法實(shí)現(xiàn)
WithValue()實(shí)現(xiàn)也是非常的簡(jiǎn)單, 偽代碼如下:
典型使用案例
下面示例程序展示valueCtx的用法:
上例main()中通過(guò)WithValue()方法獲得一個(gè)context,需要指定一個(gè)父context、key和value。然后通將該 context傳遞給子協(xié)程HandelRequest,子協(xié)程可以讀取到context的key-value。 注意:本例中子協(xié)程無(wú)法自動(dòng)結(jié)束,因?yàn)閏ontext是不支持cancle的,也就是說(shuō)<-ctx.Done()永遠(yuǎn)無(wú)法返回。
如果需要返回,需要在創(chuàng)建context時(shí)指定一個(gè)可以cancel的context作為父節(jié)點(diǎn),使用父節(jié)點(diǎn)的cancel()在適當(dāng)?shù)?時(shí)機(jī)結(jié)束整個(gè)context。
總結(jié)
Context僅僅是一個(gè)接口定義,跟據(jù)實(shí)現(xiàn)的不同,可以衍生出不同的context類(lèi)型;
cancelCtx實(shí)現(xiàn)了Context接口,通過(guò)WithCancel()創(chuàng)建cancelCtx實(shí)例; timerCtx實(shí)現(xiàn)了Context接口,通過(guò)WithDeadline()和WithTimeout()創(chuàng)建timerCtx實(shí)例;
valueCtx實(shí)現(xiàn)了Context接口,通過(guò)WithValue()創(chuàng)建valueCtx實(shí)例;
三種context實(shí)例可互為父節(jié)點(diǎn),從而可以組合成不同的應(yīng)用形式;
到此這篇關(guān)于Golang并發(fā)控制的方法的文章就介紹到這了,更多相關(guān)Golang并發(fā)控制內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
GO語(yǔ)言開(kāi)發(fā)環(huán)境搭建過(guò)程圖文詳解
這篇文章主要介紹了GO語(yǔ)言開(kāi)發(fā)環(huán)境搭建過(guò)程圖文詳解,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-01-01通過(guò)源碼分析Golang?cron的實(shí)現(xiàn)原理
golang實(shí)現(xiàn)定時(shí)任務(wù)很簡(jiǎn)單,只須要簡(jiǎn)單幾步代碼即可以完成,最近在做了幾個(gè)定時(shí)任務(wù),想研究一下它內(nèi)部是怎么實(shí)現(xiàn)的,所以將源碼過(guò)了一遍,記錄和分享在此。需要的朋友可以參考以下內(nèi)容,希望對(duì)大家有幫助2022-10-10Go語(yǔ)言metrics應(yīng)用監(jiān)控指標(biāo)基本使用說(shuō)明
這篇文章主要為大家介紹了Go語(yǔ)言metrics應(yīng)用監(jiān)控指標(biāo)的基本使用說(shuō)明,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步2022-02-02Golang Mutex實(shí)現(xiàn)互斥的具體方法
Mutex是Golang常見(jiàn)的并發(fā)原語(yǔ),在開(kāi)發(fā)過(guò)程中經(jīng)常使用到,本文主要介紹了Golang Mutex實(shí)現(xiàn)互斥的具體方法,具有一定的參考價(jià)值,感興趣的可以了解一下2023-04-04基于Go和PHP語(yǔ)言實(shí)現(xiàn)爬樓梯算法的思路詳解
這篇文章主要介紹了Go和PHP 實(shí)現(xiàn)爬樓梯算法,本文通過(guò)動(dòng)態(tài)規(guī)劃和斐波那契數(shù)列兩種解決思路給大家講解的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-05-05淺談Golang?Slice切片如何擴(kuò)容的實(shí)現(xiàn)
本文主要介紹了淺談Golang?Slice切片如何擴(kuò)容的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-02-02如何在Go語(yǔ)言中高效使用Redis的Pipeline
在 Redis 中,Pipeline 就像一條流水線,它允許我們將多個(gè)命令一次性發(fā)送到服務(wù)器,下面我們就來(lái)看看如何在Go語(yǔ)言中高效使用Redis的Pipeline吧2024-11-11windows下使用GoLand生成proto文件的方法步驟
本文主要介紹了windows下使用GoLand生成proto文件的方法步驟,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-06-06