Golang 協程Goroutine到底是怎麼回事?(二)

上一篇從協程的通用原理講起,講了通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
  1. 當加鎖失敗,則保存上下文,把自己賦值到一個sudog結構裏
  2. 掛接到鎖內部相關隊列裏(semaRoot),root.queue() 。
  3. 調用goparkunlock主動切走,切到調度協程
sync.Mutex.Unlock
-> runtime_Semrelease
    -> sync_runtime_Semrelease
        -> semrelease1
  1. 解鎖
  2. 取出這個鎖內部等待隊列的一個元素(g)
  3. 調用goready喚醒goroutine,投入隊列中,等待執行

image

現在就以A, B任務同時處理Z來舉例:

  1. A因爲要修改Z,所以加了協程鎖
  2. 加鎖之後,由於處理一些其他的邏輯,因爲某些等待事件,又把cpu切到M.g0調度了 (yield);注意了還沒有放鎖
  3. 這個時候M把B拿過來執行,yield to B
  4. B也要修改Z,這個時候發現鎖已經被加上了,於是把自己掛到鎖結構裏面去
  5. 然後B直接切走,yield to M.g0
  6. 現在A的事件滿足了,M.g0 重新調度到A執行,yield to A
  7. A 從剛剛切走的地方開始執行,然後放鎖
    1. 注意了,放鎖這裏就會把B這個協程任務從鎖隊列中摘除,加到調度隊列中,
  8. A執行完成之後,M.g0 調度B執行
  9. B從剛剛加鎖的地方喚醒,於是加上鎖了。然後走鎖內邏輯,走完就放鎖

以上就是協程鎖的實現原理。保證A,B在修改Z的時候必須串行化。(旁白:加鎖其實就是入隊,串行入隊,解鎖就是出隊,串行出隊喚醒)

timer

time的實現原理:

  1. time.Sleep()的時候先創建好timer結構體,掛到哈希表
  2. 確保創建了一個goroutine(timeproc),這個會不斷檢查超時的timer
  3. 調用gopark保存棧,切到調度
  4. timeproc循環檢查,當發現有超時的timer的時候,調用goready,把這個掛到運行隊列裏,等待運行

系統調用

對於某些系統調用,可能是會導致阻塞的,所以這個也必須封裝才能讓goroutine有讓出cpu的機會。go內部實現系統調用會在前後包裝兩個函數:

entersyscall
exitsyscall

解決syscall可能導致的問題關鍵就在這兩個函數。這兩個函數主要做了這些事情

entersyscall

  1. 設置p的狀態爲 _Psyscall

  2. 暫時解除P->M的綁定。但是M是有路徑找到P的。並且雖然解除了P->M的綁定,但是這裏並不會把P綁定到其他的M

exitsyscall

  1. 先嚐試綁定到之前P

  2. 如果之前的P已經被sysmon處理掉了,那麼則挑選一個空閒的P

  3. 如果還不行,則掛到全局隊列sched裏面去

(旁白:封裝這兩個函數,就是爲了監控,不能讓這一個系統調用阻塞了隊列裏所有的任務。你不能執行P了,就讓給別人,就是這個思路)

sysmon線程就是處理_Psyscall狀態的P,發現有超時的,則把P找個空閒的綁定,去執行P隊列裏的協程任務。

G-M鎖定

golang支持了一個G-M鎖定的功能,通過lockOSThread和unlockOSThread來實現。主要是用於一些cgo調用,或者一些特殊的庫,有些庫是要求固定在一個線程上跑。

  1. G_a鎖定M0 lockOSThread

  2. G_a調用gosched切走,投入P1隊列

  3. M0調度,發現是lockedm,於是讓出P0,自己調用notesleep睡眠

  4. M1取出G_a,發現是lockedg,於是讓出P1給M0,並且喚醒M0. 自己變idle,stopm休眠

  5. 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的異步,配合協程一起使用。

image


堅持思考,方向比努力更重要。關注我:奇伢雲存儲

image

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章