C語言迴圈結構詳解 在C語言中,迴圈結構是一種重要的控制結構,它允許我們重覆執行一段代碼,以達到特定的目的。迴圈結構可以幫助我們簡化重覆性的任務,提高代碼的效率。本篇文章將深入探討C語言中的迴圈結構,包括 while 迴圈、for 迴圈、do-while 迴圈以及迴圈中的控制語句。 1. while ...
互斥鎖的定義
type Mutex struct {
state int32
sema uint32
}
一個 sema,背後實際上 是一個 休眠隊列,可以看下上篇。
一個state,這個狀態 分為4個部分。
後三位 各自代表一個狀態。 前29位代表最大可等待協程的個數。
state的結構
locked 是否加鎖 1加鎖,0 正常 占1位
woken 是否醒來 占1位
starving 是否饑餓模式 占1位
waiterShift 等待的數量 占29位
底層的定義,下麵看代碼時候,會說明。
正常模式
加鎖
假設現在來了2個g,都想加鎖,但是只有一個能成功,2個都通過 atomic.CompareAndSwapInt32(lock, 0 ,1) 偽代碼
去更改 locked 位置。
改成功的g獲取了鎖,沒成功的g先自旋幾次,然後如果還是未獲取到鎖,則進入
sema
休眠隊列。
未成功的g進入休眠隊列,把waiterShift加1。
通過這個結論,看代碼驗證下:
mutexLocked = 1 << iota // mutex is locked
mutexWoken
mutexStarving
mutexWaiterShift = iota
func (m *Mutex) Lock() {
// 先給state的最後一位 寫 1
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
if race.Enabled {
race.Acquire(unsafe.Pointer(m))
}
寫上了, 加鎖成功,直接返回。
return
}
// 寫不上進入這個方法
m.lockSlow()
}
// 不是完整代碼,只截取和這裡相關的部分
func (m *Mutex) lockSlow() {
starving := false
iter := 0
old := m.state
for {
// 是否是饑餓模式 是否還能自旋 iter會記錄自旋次數
if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
runtime_doSpin()
iter++ // 自旋次數加1
old = m.state
continue
}
// 自旋一定次數後
new := old
// 判斷是否是饑餓模式
if old&mutexStarving == 0 {
new |= mutexLocked
}
if atomic.CompareAndSwapInt32(&m.state, old, new) {
runtime_SemacquireMutex(&m.sema, queueLifo, 1)
// 進入了休眠 ,不會執行下麵的語句了。直到被喚醒
starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
}
}
小結:
嘗試CAS直接加鎖
若無法直接獲取,進行多次自旋嘗試
多次嘗試失敗,進入sema隊列休眠
如果這個時候,再來一個:
也是同樣,進入sema的休眠隊列。
解鎖
解鎖的這個g
,除了修改locked
的值,還需要去判斷waiterShift
,有沒有協程在等,如果有,要去喚醒一個協程。
看代碼:
func (m *Mutex) Unlock() {
// 減去1,發現state的值,不是0,說明有協程在等
new := atomic.AddInt32(&m.state, -mutexLocked)
if new != 0 {
m.unlockSlow(new)
}
}
func (m *Mutex) unlockSlow(new int32) {
if new&mutexStarving == 0 { // 這裡是講了 非饑餓模式
old := new
for {
new = (old - 1<<mutexWaiterShift) | mutexWoken
if atomic.CompareAndSwapInt32(&m.state, old, new) {
runtime_Semrelease(&m.sema, false, 1) // 從 sema中釋放一個 g
return
}
old = m.state
}
}
正常模式比較好理解:
如果一個g先加鎖成功,則別的g進來後,先自旋等待一下,然後進入sema休眠隊列。
等到g解鎖時候,回去釋放sema休眠隊列中的一個g,這個隊列是平衡樹。
mutex正常模式:自旋加鎖+sema休眠等待
饑餓模式
假設g解鎖後,釋放了一個g出來。現在 mutex
的locked
位置為0
。 這個時候,又來了2個g,那剛剛釋放的g不一定能競爭得過來的這兩個g。
為瞭解決這個問題,go設置了鎖饑餓模式:
當前協程等待鎖的時間超過了 1ms,切換到饑餓模式
饑餓模式中,不自旋,新來的協程直接sema休眠
饑餓模式中,被喚醒的協程直接獲取鎖
沒有協程在隊列中繼續等待時,回到正常模式
把starving置為1
新過來的協程直接休眠,喚醒的協程直接獲得鎖
代碼: 有點長 要結合裡面的for迴圈,看兩遍
func (m *Mutex) lockSlow() {
var waitStartTime int64
starving := false
awoke := false
iter := 0
old := m.state
for {
// 饑餓模式不自旋了
if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
runtime_doSpin()
iter++ // 自旋次數加1
old = m.state
continue
}
new := old
// 判斷是否是饑餓模式
if old&mutexStarving == 0 {
new |= mutexLocked
}
// 如果是饑餓模式,給waiterShift 加1
if old&(mutexLocked|mutexStarving) != 0 {
new += 1 << mutexWaiterShift
}
// starving 現在為 true了
if starving && old&mutexLocked != 0 {
new |= mutexStarving
}
// 寫入饑餓模式的狀態 new 現在為饑餓模式了
if atomic.CompareAndSwapInt32(&m.state, old, new) {
// 新進來的g,直接休眠
runtime_SemacquireMutex(&m.sema, queueLifo, 1)
//starvationThresholdNs = 1e6 1毫秒
// 喚醒的g,從這裡開始執行, 判斷g等待的時間 ,超過了1毫秒 starving 置為 true
starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
old = m.state
if old&mutexStarving != 0 {
delta := int32(mutexLocked - 1<<mutexWaiterShift)
if !starving || old>>mutexWaiterShift == 1 {
delta -= mutexStarving
}
// 直接改為 g已經獲取鎖的值,直接寫入
atomic.AddInt32(&m.state, delta)
break
}
awoke = true
iter = 0
} else {
old = m.state
}
}
}
總結:
鎖競爭嚴重時,互斥鎖進入饑餓模式
饑餓模式沒有自旋等待,有利於公平,見過有人叫 公平鎖 。
使用經驗
1. 減少鎖的使用時間,lock和unlock 之間,業務要精簡,只放必須的代碼。
2. 善用defer確保鎖的釋放。避免忘記釋放 例如走到if這樣分支,最後沒有釋放鎖。
思考一個問題:
加鎖、開鎖其實就是用的 atomic 操作一個 值,開發者也能實現,為什麼還要用鎖 ?
結合上幾篇講的 sema 和 協程搶占的內容,這樣做是能夠做到鎖住一段代碼,但是,未獲取鎖的g,無法做到休眠、喚醒的功能。 所以,系統才的 mutex 採用 atomic和 sema的結合。