Golang 協(xié)程Goroutine到底是怎么回事?(二)
“?協(xié)程不僅只有調(diào)度,還需要配套的齊全設(shè)備,比如協(xié)程鎖,定時器,條件變量等”
上一篇從協(xié)程的通用原理講起,講了通Golang的協(xié)程,使用一個完成的協(xié)程,必須要配合完善的配套設(shè)備,協(xié)程鎖,定時器等,這篇文章就是描述于此。
Golang 協(xié)程鎖,定時器,是怎么回事?系統(tǒng)調(diào)用又有什么特殊,G-M鎖定是什么?
協(xié)程鎖
之前提到,協(xié)程使用之后,是必須配套實現(xiàn)一些配件的。關(guān)鍵就是要保證在執(zhí)行g(shù)oroutine的時候不阻塞。最典型的的就是鎖、timer、系統(tǒng)調(diào)用這三個方面。其中鎖必須要是協(xié)程鎖。
舉例:某個場景,任務(wù)A需要修改Z,任務(wù)B也需要修改Z。如果是串行系統(tǒng),A執(zhí)行完了,再執(zhí)行B,那么不會有問題。A -> B ?,F(xiàn)在A,B是goroutine,可以并發(fā)執(zhí)行,那么在操作Z的時候我們必須要有保證串行化的機制。
CO_LOCK{#處理邏輯}CO_UNLOCK
現(xiàn)在的關(guān)鍵點就是,我們不能直接用之前的mutex鎖,或者是自旋鎖。這樣會嚴重影響并發(fā),或者導(dǎo)致死鎖。而必須配套實現(xiàn)協(xié)程鎖。
sync.Mutex.Lock-> runtime_SemacquireMutex-> sync_runtime_SemacquireMutex-> semacquire1 // runtime/sema.go
當加鎖失敗,則保存上下文,把自己賦值到一個sudog結(jié)構(gòu)里
掛接到鎖內(nèi)部相關(guān)隊列里(semaRoot),root.queue() 。
調(diào)用goparkunlock主動切走,切到調(diào)度協(xié)程
sync.Mutex.Unlock-> runtime_Semrelease-> sync_runtime_Semrelease-> semrelease1
解鎖
取出這個鎖內(nèi)部等待隊列的一個元素(g)
調(diào)用goready喚醒goroutine,投入隊列中,等待執(zhí)行?

現(xiàn)在就以A, B任務(wù)同時處理Z來舉例:
A因為要修改Z,所以加了協(xié)程鎖
加鎖之后,由于處理一些其他的邏輯,因為某些等待事件,又把cpu切到M.g0調(diào)度了 (yield);注意了還沒有放鎖
這個時候M把B拿過來執(zhí)行,yield to B
B也要修改Z,這個時候發(fā)現(xiàn)鎖已經(jīng)被加上了,于是把自己掛到鎖結(jié)構(gòu)里面去
然后B直接切走,yield to M.g0
現(xiàn)在A的事件滿足了,M.g0 重新調(diào)度到A執(zhí)行,yield to A
A 從剛剛切走的地方開始執(zhí)行,然后放鎖
注意了,放鎖這里就會把B這個協(xié)程任務(wù)從鎖隊列中摘除,加到調(diào)度隊列中,
A執(zhí)行完成之后,M.g0 調(diào)度B執(zhí)行
B從剛剛加鎖的地方喚醒,于是加上鎖了。然后走鎖內(nèi)邏輯,走完就放鎖
以上就是協(xié)程鎖的實現(xiàn)原理。保證A,B在修改Z的時候必須串行化。(旁白:加鎖其實就是入隊,串行入隊,解鎖就是出隊,串行出隊喚醒)
timer
time的實現(xiàn)原理:
time.Sleep()的時候先創(chuàng)建好timer結(jié)構(gòu)體,掛到哈希表
確保創(chuàng)建了一個goroutine(timeproc),這個會不斷檢查超時的timer
調(diào)用gopark保存棧,切到調(diào)度
timeproc循環(huán)檢查,當發(fā)現(xiàn)有超時的timer的時候,調(diào)用goready,把這個掛到運行隊列里,等待運行
系統(tǒng)調(diào)用
對于某些系統(tǒng)調(diào)用,可能是會導(dǎo)致阻塞的,所以這個也必須封裝才能讓goroutine有讓出cpu的機會。go內(nèi)部實現(xiàn)系統(tǒng)調(diào)用會在前后包裝兩個函數(shù):
entersyscallexitsyscall
解決syscall可能導(dǎo)致的問題關(guān)鍵就在這兩個函數(shù)。這兩個函數(shù)主要做了這些事情
entersyscall
設(shè)置p的狀態(tài)為 _Psyscall
暫時解除P->M的綁定。但是M是有路徑找到P的。并且雖然解除了P->M的綁定,但是這里并不會把P綁定到其他的M
exitsyscall
先嘗試綁定到之前P
如果之前的P已經(jīng)被sysmon處理掉了,那么則挑選一個空閑的P
如果還不行,則掛到全局隊列sched里面去
(旁白:封裝這兩個函數(shù),就是為了監(jiān)控,不能讓這一個系統(tǒng)調(diào)用阻塞了隊列里所有的任務(wù)。你不能執(zhí)行P了,就讓給別人,就是這個思路)
sysmon線程就是處理_Psyscall狀態(tài)的P,發(fā)現(xiàn)有超時的,則把P找個空閑的綁定,去執(zhí)行P隊列里的協(xié)程任務(wù)。?
G-M鎖定
golang支持了一個G-M鎖定的功能,通過lockOSThread和unlockOSThread來實現(xiàn)。主要是用于一些cgo調(diào)用,或者一些特殊的庫,有些庫是要求固定在一個線程上跑。
G_a鎖定M0 lockOSThread
G_a調(diào)用gosched切走,投入P1隊列
M0調(diào)度,發(fā)現(xiàn)是lockedm,于是讓出P0,自己調(diào)用notesleep睡眠
M1取出G_a,發(fā)現(xiàn)是lockedg,于是讓出P1給M0,并且喚醒M0.?自己變idle,stopm休眠
M0繼續(xù)執(zhí)行G_a
你可以發(fā)現(xiàn),G_a只在M0上運行,鎖定這段期間,M0也只執(zhí)行了G_a任務(wù)。?
當前go有哪些問題
當前go沒有實現(xiàn)異步io。換句話說,如果在一個goroutine里面使用read/write io的系統(tǒng)調(diào)用,這些都是同步的io調(diào)用。會實實在在的阻塞M的調(diào)度,在遇到io延遲慢的時候,會導(dǎo)致sysmon檢查到M-P超時(10ms),那么就會把M-P解綁,M游離出去執(zhí)行阻塞任務(wù),分配一個新的M來綁定P執(zhí)行隊列里的任務(wù)。
那么這種情況,雖然沒有完全阻塞死P任務(wù)的執(zhí)行,但是代價非常大,而且可能會導(dǎo)致M的數(shù)量一直飆升。就算沒有這些極限情況,IO的并發(fā)能力相較于aio也是不行的。(旁白:Golang能切走的當前只有網(wǎng)絡(luò)IO,磁盤io走的是系統(tǒng)調(diào)用,協(xié)程切不走)
當前net庫是已經(jīng)實現(xiàn)了底層的patch,aio還沒有實現(xiàn)關(guān)鍵還是aio的復(fù)雜性導(dǎo)致的。?其實很多的工程實踐是通過libaio來實現(xiàn)磁盤io的異步,配合協(xié)程一起使用。

推薦閱讀
