詳解Go如何實(shí)現(xiàn)協(xié)程并發(fā)執(zhí)行
順序執(zhí)行有什么問(wèn)題
很明顯,順序執(zhí)行會(huì)造成協(xié)程的饑餓問(wèn)題。如果某個(gè)大協(xié)程掛在線(xiàn)程中運(yùn)行了十分鐘,那么隊(duì)列中其它協(xié)程就一直處于休眠中無(wú)法運(yùn)行,這不公平。如果讓某些實(shí)時(shí)性強(qiáng)的協(xié)程饑餓,得不到cpu運(yùn)行,會(huì)影響業(yè)務(wù)。比如視頻彈幕,用戶(hù)發(fā)出一條彈幕,得盡快顯示在視頻中。若此時(shí)協(xié)程饑餓,得不到處理,用戶(hù)體驗(yàn)就差了。
該如何解決呢?簡(jiǎn)單,讓大協(xié)程切換出去就可以了。
協(xié)程切換
回到線(xiàn)程循環(huán)這張圖中(在深入考究協(xié)程一文中有解釋?zhuān)?,業(yè)務(wù)方法這塊即線(xiàn)程執(zhí)行的協(xié)程。如果業(yè)務(wù)方法運(yùn)行時(shí)間過(guò)長(zhǎng),則觸發(fā)協(xié)程切換。
- 對(duì)協(xié)程:保存該協(xié)程運(yùn)行的情況,然后將該協(xié)程放入本地隊(duì)列隊(duì)尾,休眠該協(xié)程。
- 對(duì)線(xiàn)程:從業(yè)務(wù)方法中跳出,重新執(zhí)行
schedule方法,之后會(huì)從本地隊(duì)列中獲取一個(gè)新的協(xié)程運(yùn)行。

但這樣只是本地隊(duì)列的協(xié)程切換,全局隊(duì)列的協(xié)程仍會(huì)饑餓,該如何解決呢?
隨機(jī)抽取全局協(xié)程
在線(xiàn)程循環(huán)的 shedule 的 findRunnable 函數(shù)中,每隔一段時(shí)間就會(huì)從全局隊(duì)列中獲取一個(gè)協(xié)程放到本地隊(duì)列,再通過(guò)本地隊(duì)列的協(xié)程切換,使得來(lái)自全局隊(duì)列的協(xié)程有機(jī)會(huì)運(yùn)行,從而解決全局隊(duì)列協(xié)程的饑餓問(wèn)題。來(lái)看下源碼:
if pp.schedtick%61 == 0 && sched.runqsize > 0 {
lock(&sched.lock)
gp := globrunqget(pp, 1)
unlock(&sched.lock)
if gp != nil {
return gp, false, false
}
}pp.schedtick 表示線(xiàn)程循環(huán)的次數(shù),如果達(dá)到61的倍數(shù),就執(zhí)行 globrunqget ,從全局隊(duì)列中獲取協(xié)程。
協(xié)程如何并發(fā)執(zhí)行
從以上可得知,線(xiàn)程通過(guò)切換協(xié)程的方式,不再順序的執(zhí)行協(xié)程了,從而達(dá)到并發(fā)執(zhí)行協(xié)程的效果。這關(guān)鍵在于協(xié)程的切換,那協(xié)程在什么時(shí)候會(huì)切換呢?
協(xié)程切換時(shí)機(jī)
協(xié)程的切換時(shí)機(jī)如下:
- 主動(dòng)掛起,調(diào)用
gopark函數(shù),使協(xié)程主動(dòng)休眠等待 - 系統(tǒng)調(diào)用完成后,io操作耗時(shí),因此切換協(xié)程
- 基于協(xié)作的搶占式調(diào)度,協(xié)程在跳轉(zhuǎn)到其它方法時(shí),就把自己切換出去
- 基于信號(hào)的搶占式調(diào)度,通過(guò)發(fā)送信號(hào),觸發(fā)線(xiàn)程的調(diào)度方法
主動(dòng)掛起
協(xié)程可以調(diào)用 runtime.gopark 方法,使自己陷入休眠。

源碼如下:
// 將當(dāng)前協(xié)程置于等待狀態(tài)
func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason waitReason, traceEv byte, traceskip int) {
if reason != waitReasonSleep {
checkTimeouts() // timeouts may expire while two goroutines keep the scheduler busy
}
mp := acquirem()
gp := mp.curg
status := readgstatus(gp)
if status != _Grunning && status != _Gscanrunning {
throw("gopark: bad g status")
}
mp.waitlock = lock
mp.waitunlockf = unlockf
gp.waitreason = reason
mp.waittraceev = traceEv
mp.waittraceskip = traceskip
releasem(mp)
// can't do anything that might move the G between Ms here.
mcall(park_m)
}可以看到:
gopark中通過(guò)acquirem獲取到當(dāng)前的線(xiàn)程指針mp- 通過(guò)mp獲取到當(dāng)前運(yùn)行的協(xié)程指針gp
- 給mp,gp的一些字段賦值,修改狀態(tài)
- 然后調(diào)用
mcall,mcall是一個(gè)匯編方法,作用時(shí)切換到g0棧,并執(zhí)行傳入的函數(shù)。這里執(zhí)行park_m函數(shù),最終跳轉(zhuǎn)到schedule方法,也就是線(xiàn)程循環(huán)的開(kāi)頭,實(shí)現(xiàn)了協(xié)程的主動(dòng)切換。
// park_m函數(shù)最終跳轉(zhuǎn)到schedule
func park_m(gp *g) {
mp := getg().m
...
schedule()
}由于gopark是小寫(xiě)開(kāi)頭的,外部無(wú)法調(diào)用。我們?cè)谑褂?time.Sleep , sync.WaitGroup 時(shí),會(huì)間接的使用到gopark,將協(xié)程休眠。
系統(tǒng)調(diào)用完成后
當(dāng)協(xié)程要執(zhí)行讀寫(xiě)文件、網(wǎng)絡(luò) IO、進(jìn)程間通信等系統(tǒng)調(diào)用的操作時(shí),會(huì)進(jìn)入 entersyscall 函數(shù),將該協(xié)程暫停并放入等待隊(duì)列。
當(dāng)系統(tǒng)調(diào)用完成后,由于io操作都比較耗時(shí),說(shuō)明該協(xié)程已經(jīng)運(yùn)行了挺長(zhǎng)一段時(shí)間了,因此將協(xié)程掛起,切換另一個(gè)協(xié)程執(zhí)行很合理。

而 exitsyscall 也位于runtime中,源碼部分如下:
func exitsyscall() {
gp := getg()
...
mcall(exitsyscall0)
...
}又是熟悉的 mcall ,mcall執(zhí)行了 exitsyscall0 函數(shù),最終跳轉(zhuǎn)到線(xiàn)程循環(huán)開(kāi)頭的 schedule 函數(shù),完成協(xié)程切換。
基于協(xié)作的搶占式調(diào)度
如果協(xié)程既不主動(dòng)掛起,也沒(méi)有進(jìn)行系統(tǒng)調(diào)用呢,那就一直切換不出去了?該怎么解決呢,如果每個(gè)協(xié)程都經(jīng)常調(diào)用同一個(gè)方法的話(huà),那就可以在這個(gè)方法里加入一個(gè)鉤子,讓這個(gè)協(xié)程切換出去。
思路有了,具體找哪個(gè)方法呢?這里做一個(gè)演示。
package main
import (
"fmt"
"time"
)
func do1() {
do2()
}
func do2() {
do3()
}
func do3() {
fmt.Println("do3")
}
func main() {
go do1()
time.Sleep(time.Hour)
}以上代碼開(kāi)啟一個(gè)do1協(xié)程,do1調(diào)用do2,do2調(diào)用do3。我們通過(guò) go build -gcflags -S main.go 命令,查看匯編代碼,發(fā)現(xiàn)多次調(diào)用到了 runtime.morestack_noctxt 方法。在函數(shù)跳轉(zhuǎn)的時(shí)候,編譯器會(huì)插入 runtime.morestack_noctxt 這個(gè)方法。目的是檢查函數(shù)??臻g是否足夠。 簡(jiǎn)略源碼如下:
TEXT runtime·morestack_noctxt(SB),NOSPLIT,$0 MOVL $0, DX JMP runtime·morestack(SB)
TEXT runtime·morestack(SB),NOSPLIT|NOFRAME,$0-0 ... BL runtime·newstack(SB) ...
最終調(diào)用到 newstack 這個(gè)go方法。
現(xiàn)在對(duì)于運(yùn)行時(shí)間超過(guò)10ms的大協(xié)程,其 g.stackguard0 會(huì)被賦值為 stackPreempt ,意味著該協(xié)程要切換出去了。
stackPreempt值為 0xfffffade
// 0xfffffade in hex. const stackPreempt = uintptrMask & -1314
于是在 newstack 方法中會(huì)判斷 g.stackguard0 是否為 stackPreempt ,是則將該協(xié)程切換出去。
func newstack() {
// 判斷是否有搶占信號(hào)
preempt := stackguard0 == stackPreempt
...
if preempt {
...
// Act like goroutine called runtime.Gosched.
gopreempt_m(gp) // never return
}
...
}func gopreempt_m(gp *g) {
...
goschedImpl(gp)
}func goschedImpl(gp *g) {
...
schedule()
}以上流程總結(jié)來(lái)說(shuō):
- Go對(duì)大協(xié)程會(huì)把g.stackguard0標(biāo)記為stackPreempt。
- 在大協(xié)程調(diào)用其它函數(shù)時(shí),會(huì)調(diào)用newstack判斷??臻g,順便判斷該協(xié)程是否要切換出去。
- 要切換則進(jìn)入gopreempt_m -> goschedImpl -> schedule,最終回到線(xiàn)程循環(huán)的開(kāi)頭。
流程圖如下:

基于信號(hào)的搶占式調(diào)度
如果協(xié)程不主動(dòng)掛起,不系統(tǒng)調(diào)用,不調(diào)用其它函數(shù),只是純計(jì)算的任務(wù),那該如何切換呢?如下:
go func() {
i := 0
for {
i++
}
}()Go就利用了操作系統(tǒng)通信的方式,通過(guò)GC的線(xiàn)程向該協(xié)程對(duì)應(yīng)的線(xiàn)程發(fā)送信號(hào),觸發(fā)該線(xiàn)程的切換方法。具體步驟為:
- 注冊(cè)
SIGURG信號(hào)的處理函數(shù) - GC線(xiàn)程工作時(shí),向該目標(biāo)線(xiàn)程發(fā)送信號(hào)
- 線(xiàn)程接收信號(hào)后,觸發(fā)調(diào)度方法
流程圖如下:

源碼分析:
線(xiàn)程接收到操作系統(tǒng)信號(hào),進(jìn)入 sighandler 方法,識(shí)別信號(hào)為SIGURG,進(jìn)入 doSigPreempt 方法。 之后流程:doSigPreempt -> asyncPreempt -> asyncPreempt2 -> mcall -> gopreempt_m -> goschedImpl。 最終調(diào)用schedule方法,回到線(xiàn)程開(kāi)頭,完成協(xié)程切換。
具體細(xì)節(jié)各位可以動(dòng)手查看下,感悟更多。
總結(jié)
要使協(xié)程并發(fā)執(zhí)行,那各個(gè)線(xiàn)程就不能順序的執(zhí)行協(xié)程,得選擇合適的時(shí)機(jī)將協(xié)程切換出去,換另一個(gè)協(xié)程執(zhí)行。因此切換時(shí)機(jī)就特別重要了,所以本篇重點(diǎn)講解了四種切換方式,分別為:
- 協(xié)程主動(dòng)掛起,調(diào)用
gopark函數(shù),使協(xié)程主動(dòng)休眠等待 - 系統(tǒng)調(diào)用完成后,由于io操作挺耗時(shí),代表該協(xié)程運(yùn)行太久了,因此切換協(xié)程
- 基于協(xié)作的搶占式調(diào)度,協(xié)程運(yùn)行超10ms,就標(biāo)記為搶占。這時(shí)協(xié)程在跳轉(zhuǎn)到其它方法時(shí),就把自己切換出去
- 基于信號(hào)的搶占式調(diào)度,協(xié)程純自閉,得外部干擾。因此通過(guò)GC線(xiàn)程發(fā)送信號(hào),觸發(fā)線(xiàn)程的調(diào)度方法
以上就是詳解Go如何實(shí)現(xiàn)協(xié)程并發(fā)執(zhí)行的詳細(xì)內(nèi)容,更多關(guān)于Go協(xié)程并發(fā)執(zhí)行的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- golang并發(fā)編程中Goroutine 協(xié)程的實(shí)現(xiàn)
- 并發(fā)安全本地化存儲(chǔ)go-cache讀寫(xiě)鎖實(shí)現(xiàn)多協(xié)程并發(fā)訪(fǎng)問(wèn)
- Go?并發(fā)編程協(xié)程及調(diào)度機(jī)制詳情
- go語(yǔ)言限制協(xié)程并發(fā)數(shù)的方案詳情
- Go并發(fā):使用sync.WaitGroup實(shí)現(xiàn)協(xié)程同步方式
- 詳解Go多協(xié)程并發(fā)環(huán)境下的錯(cuò)誤處理
- Go 并發(fā)實(shí)現(xiàn)協(xié)程同步的多種解決方法
- Go 控制協(xié)程(goroutine)的并發(fā)數(shù)量
相關(guān)文章
Go語(yǔ)言題解LeetCode1260二維網(wǎng)格遷移示例詳解
這篇文章主要為大家介紹了Go語(yǔ)言題解LeetCode1260二維網(wǎng)格遷移示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01
使用Gin框架搭建一個(gè)Go Web應(yīng)用程序的方法詳解
在本文中,我們將要實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 Web 應(yīng)用程序,通過(guò) Gin 框架來(lái)搭建,主要支持用戶(hù)注冊(cè)和登錄,用戶(hù)可以通過(guò)注冊(cè)賬戶(hù)的方式創(chuàng)建自己的賬號(hào),并通過(guò)登錄功能進(jìn)行身份驗(yàn)證,感興趣的同學(xué)跟著小編一起來(lái)看看吧2023-08-08
go語(yǔ)言限制協(xié)程并發(fā)數(shù)的方案詳情
一個(gè)線(xiàn)程中可以有任意多個(gè)協(xié)程,但某一時(shí)刻只能有一個(gè)協(xié)程在運(yùn)行,多個(gè)協(xié)程分享該線(xiàn)程分配到的計(jì)算機(jī)資源,接下來(lái)通過(guò)本文給大家介紹go語(yǔ)言限制協(xié)程的并發(fā)數(shù)的方案詳情,感興趣的朋友一起看看吧2022-01-01
Go語(yǔ)言開(kāi)發(fā)保證并發(fā)安全實(shí)例詳解
這篇文章主要為大家介紹了Go語(yǔ)言開(kāi)發(fā)保證并發(fā)安全實(shí)例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09
Go開(kāi)源項(xiàng)目分布式唯一ID生成系統(tǒng)
這篇文章主要為大家介紹了Go開(kāi)源項(xiàng)目分布式唯一ID生成系統(tǒng)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06

