上一篇從協程的通用原理講起,講了通Golang的協程,使用一個完成的協程,必須要配合完善的配套設備,協程鎖,定時器等,這篇文章就是描述於此。
Go 協程配套設備
Golang 協程鎖,定時器,是怎麼回事?系統調用又有什麼特殊,G-M鎖定是什麼?
協程鎖
之前提到,協程使用之後,是必須配套實現一些配件的。關鍵就是要保證在執行goroutine的時候不阻塞。最典型的的就是鎖、timer、系統調用這三個方面。其中鎖必須要是協程鎖。
舉例:某個場景,任務A需要修改Z,任務B也需要修改Z。如果是串行系統,A執行完了,再執行B,那麼不會有問題。A -> B 。現在A,B是goroutine,可以併發執行,那麼在操作Z的時候我們必須要有保證串行化的機制。
CO_LOCK
{
#處理邏輯
}
CO_UNLOCK
現在的關鍵點就是,我們不能直接用之前的mutex鎖,或者是自旋鎖。這樣會嚴重影響併發,或者導致死鎖。而必須配套實現協程鎖。
sync.Mutex.Lock
-> runtime_SemacquireMutex
-> sync_runtime_SemacquireMutex
-> semacquire1 // runtime/sema.go
- 當加鎖失敗,則保存上下文,把自己賦值到一個sudog結構裏
- 掛接到鎖內部相關隊列裏(semaRoot),root.queue() 。
- 調用goparkunlock主動切走,切到調度協程
sync.Mutex.Unlock
-> runtime_Semrelease
-> sync_runtime_Semrelease
-> semrelease1
- 解鎖
- 取出這個鎖內部等待隊列的一個元素(g)
- 調用goready喚醒goroutine,投入隊列中,等待執行
現在就以A, B任務同時處理Z來舉例:
- A因爲要修改Z,所以加了協程鎖
- 加鎖之後,由於處理一些其他的邏輯,因爲某些等待事件,又把cpu切到M.g0調度了 (yield);注意了還沒有放鎖
- 這個時候M把B拿過來執行,yield to B
- B也要修改Z,這個時候發現鎖已經被加上了,於是把自己掛到鎖結構裏面去
- 然後B直接切走,yield to M.g0
- 現在A的事件滿足了,M.g0 重新調度到A執行,yield to A
- A 從剛剛切走的地方開始執行,然後放鎖
- 注意了,放鎖這裏就會把B這個協程任務從鎖隊列中摘除,加到調度隊列中,
- A執行完成之後,M.g0 調度B執行
- B從剛剛加鎖的地方喚醒,於是加上鎖了。然後走鎖內邏輯,走完就放鎖
以上就是協程鎖的實現原理。保證A,B在修改Z的時候必須串行化。(旁白:加鎖其實就是入隊,串行入隊,解鎖就是出隊,串行出隊喚醒)
timer
time的實現原理:
- time.Sleep()的時候先創建好timer結構體,掛到哈希表
- 確保創建了一個goroutine(timeproc),這個會不斷檢查超時的timer
- 調用gopark保存棧,切到調度
- timeproc循環檢查,當發現有超時的timer的時候,調用goready,把這個掛到運行隊列裏,等待運行
系統調用
對於某些系統調用,可能是會導致阻塞的,所以這個也必須封裝才能讓goroutine有讓出cpu的機會。go內部實現系統調用會在前後包裝兩個函數:
entersyscall
exitsyscall
解決syscall可能導致的問題關鍵就在這兩個函數。這兩個函數主要做了這些事情
entersyscall
-
設置p的狀態爲 _Psyscall
-
暫時解除P->M的綁定。但是M是有路徑找到P的。並且雖然解除了P->M的綁定,但是這裏並不會把P綁定到其他的M
exitsyscall
-
先嚐試綁定到之前P
-
如果之前的P已經被sysmon處理掉了,那麼則挑選一個空閒的P
-
如果還不行,則掛到全局隊列sched裏面去
(旁白:封裝這兩個函數,就是爲了監控,不能讓這一個系統調用阻塞了隊列裏所有的任務。你不能執行P了,就讓給別人,就是這個思路)
sysmon線程就是處理_Psyscall狀態的P,發現有超時的,則把P找個空閒的綁定,去執行P隊列裏的協程任務。
G-M鎖定
golang支持了一個G-M鎖定的功能,通過lockOSThread和unlockOSThread來實現。主要是用於一些cgo調用,或者一些特殊的庫,有些庫是要求固定在一個線程上跑。
-
G_a鎖定M0 lockOSThread
-
G_a調用gosched切走,投入P1隊列
-
M0調度,發現是lockedm,於是讓出P0,自己調用notesleep睡眠
-
M1取出G_a,發現是lockedg,於是讓出P1給M0,並且喚醒M0. 自己變idle,stopm休眠
-
M0繼續執行G_a
你可以發現,G_a只在M0上運行,鎖定這段期間,M0也只執行了G_a任務。
當前go有哪些問題
當前go沒有實現異步io。換句話說,如果在一個goroutine裏面使用read/write io的系統調用,這些都是同步的io調用。會實實在在的阻塞M的調度,在遇到io延遲慢的時候,會導致sysmon檢查到M-P超時(10ms),那麼就會把M-P解綁,M遊離出去執行阻塞任務,分配一個新的M來綁定P執行隊列裏的任務。
那麼這種情況,雖然沒有完全阻塞死P任務的執行,但是代價非常大,而且可能會導致M的數量一直飆升。就算沒有這些極限情況,IO的併發能力相較於aio也是不行的。(旁白:Golang能切走的當前只有網絡IO,磁盤io走的是系統調用,協程切不走)
當前net庫是已經實現了底層的patch,aio還沒有實現關鍵還是aio的複雜性導致的。 其實很多的工程實踐是通過libaio來實現磁盤io的異步,配合協程一起使用。
堅持思考,方向比努力更重要。關注我:奇伢雲存儲