這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
概述
sync/mutex是Go語言底層基礎對象之一,用於構建多個goroutine間的同步邏輯,因此被大量高層對象所使用。
其工作模型類似於Linux核心的futex對象,具體實現極為簡潔,效能也有保證。
資料結構
type Mutex struct { state int32 sema uint32 }
mutex對象僅有兩個數值欄位,分為為state(儲存狀態)和sema(用於計算休眠goroutine數量的訊號量)。
初始化時填入的0值將mutex設定在未鎖定狀態,同時保證時間開銷最小。
這一特性允許將mutex作為其它對象的子物件使用。
state欄位
state被定義為int32類型,允許為其調用原子方法(sync/atomic),從而原子化地設定狀態。
每個state欄位均劃分三個狀態段,含義如下:
31 3 2 1 0 +----~~~----+-+-+-+ | S | |W|L| +----~~~----+-+-+-+ | | | | | | | \--- 鎖定狀態,0表示未鎖定,1表示鎖定 | | | | | \----- 喚醒事件,0表示無事件,1表示mutex已被解除鎖定,可以喚醒等待其它goroutine | | | \------- 保留位,保持為0 | \------------------- 等待喚醒以嘗試鎖定的goroutine的計數,0表示沒有等待者
方法
Lock() / 鎖定
func (m *Mutex) Lock() { // 快速路徑:直接鎖定mutex // 記為FG同步點:Fast Grab if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) { // 僅在沒有等待者、沒有喚醒事件、沒有鎖定的情況下執行 if raceenabled { raceAcquire(unsafe.Pointer(m)) } return } awoke := false for { old := m.state new := old | mutexLocked if old&mutexLocked != 0 { // 處於鎖定狀態,增加等待者計數 new = old + 1<<mutexWaiterShift } if awoke { // goroutine已被喚醒,“消費”喚醒事件,重設標誌位 new &^= mutexWoken } // 記為G同步點:Grab if atomic.CompareAndSwapInt32(&m.state, old, new) { // 新舊狀態一致,沒有被其它goroutine修改 if old&mutexLocked == 0 { // 成功鎖定 break } // 休眠等待 runtime_Semacquire(&m.sema) awoke = true } // 新舊狀態不一致,重新取狀態並嘗試鎖定 } if raceenabled { raceAcquire(unsafe.Pointer(m)) }}
Unlock() / 解除鎖定
func (m *Mutex) Unlock() { if raceenabled { _ = m.state raceRelease(unsafe.Pointer(m)) } // 快速路徑:直接解除鎖定 // 記為FD同步點:Fast Drop new := atomic.AddInt32(&m.state, -mutexLocked) if (new+mutexLocked)&mutexLocked == 0 { // 連續解除兩次以上 panic("sync: unlock of unlocked mutex") } old := new for { // 如果沒有等待者,或已經有等待者被喚醒,或已經有goroutine鎖定mutex, // 則無需嘗試喚醒 if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken) != 0 { return } // 產生喚醒事件,嘗試喚醒某個等待者 new = (old - 1<<mutexWaiterShift) | mutexWoken // 記為W同步點:Wake Up if atomic.CompareAndSwapInt32(&m.state, old, new) { // 新舊狀態一致,沒有被其它goroutine修改 // 嘗試喚醒 runtime_Semrelease(&m.sema) return } // 新舊狀態不一致,重新取狀態並嘗試喚醒 old = m.state }}
可以觀察到,兩個函數一共有四個原子函數調用點,也就是狀態同步點。
一旦原子函數調用失敗,說明狀態已被其它goroutine改變,需要重新擷取狀態以決定後續動作。
調用者
mutex不與具體goroutine掛鈎,可被任意的goroutine調用,但需要注意一下幾點:
- 同一個goroutine不能在“連續”調用Lock()函數兩次,否則會導致死結;
- 在確保調用順序正確的前提下,完全可以在一個goroutine中調用Lock(),在另一個中調用Unlock();
- 同一個mutex不能被“連續”調用Unlock()函數兩次,否則會導致異常。
狀態遷移
狀態列表
state的三個狀態段一共有如下狀態組合(-表示0):
| 狀態組合 |
休眠者 |
喚醒事件 |
鎖定 |
--- |
- |
- |
- |
--L |
- |
- |
是 |
-WL |
- |
有 |
是 |
S-L |
有 |
- |
是 |
SW- |
有 |
有 |
- |
S-- |
有 |
- |
- |
-W- |
- |
有 |
- |
SWL |
有 |
有 |
是 |
結合約步點分析,可以得到如下規則:
1. 非喚醒goroutine在嘗試鎖定時不消耗喚醒事件,對該狀態段不做改變;
2. 已喚醒goroutine一定會消耗喚醒事件,將重設該狀態段;
3. 解除鎖定後才嘗試發送喚醒事件。
遷移路徑
只有一個goroutine調用的情況
這種情況下,狀態只在---與--L之間遷移(路徑1和2),且只涉及到FG和D兩個同步點。
有兩個goroutine調用的情況
這種情況下,狀態涉及---、--L、S-L、S--、-W-、-WL六個狀態。
假設兩個goroutine分別稱為X和Y:
1. 當X已經鎖定mutex,而Y嘗試鎖定時,會從`--L`遷移到`S-L`(路徑3); 2. 當X解除鎖定後,會從`S-L`遷移到`S--`(路徑5),此時Y還在休眠中; 3. 當X發送喚醒事件後,會從`S--`狀態遷移到`-W-`(路徑7),並嘗試喚醒休眠者(即Y),此時有三條路徑: 3.1 Y成功鎖定mutex,則從`-W-`遷移到`--L`(路徑8),X若嘗試鎖定,則進一步遷移到`S-L`(路徑3); 3.2 X又嘗試鎖定mutex,則從`-W-`遷移到`-WL`(路徑14),Y因被喚醒而嘗試鎖定,則進一步遷移到`S-L`(路徑17); 3.3 X又嘗試鎖定mutex,且在Y嘗試鎖定前解除鎖定,則從`-W-`遷移到`-WL`(路徑14)而後又遷移回`-W-`(路徑15)。
有多個goroutine調用的情況
這種情況是兩個goroutine調用情況的延續,在原基礎上再涉及-WL和SWL兩個狀態。
假設三個goroutine分別稱為X、Y和Z:
1. 當X已經鎖定mutex,則Y在等待,且Z嘗試鎖定,會從`S-L`遷移到其自身(路徑4); 2. 當X解除鎖定、還未喚醒Y時,此時若Z嘗試鎖定,會從`S--`遷移回`S-L`(路徑6); 3. 當X解除鎖定、且喚醒Y後(路徑9),此時若Z嘗試鎖定,會從`SW-`遷移到`SWL`(路徑11),此時有三條路徑: 3.1 Y發現mutex已被鎖定,而進一步遷移到`S-L`狀態(路徑18); 3.2 Z迅速解除鎖定,狀態遷移回`SW-`(路徑12),Y嘗試鎖定,進一步遷移到`S-L`(路徑10); 3.3 更多的goroutine嘗試鎖定,會一直遷移回`SWL`(路徑13)。4. 當X喚醒Y、且Z嘗試鎖定成功,則從`-WL`遷移到`SWL`(路徑16)。
附錄
附狀態遷移圖的Graphviz源碼。
graph { node [shape="circle"]; edge [dir="forward", len="2.0"]; rankdir=LR; nnn [label="---"]; nnl [label="--L"]; swn [label="SW-"]; snn [label="S--", pos="0,0"]; snl [label="S-L"]; nwn [label="-W-"]; nwl [label="-WL"]; swl [label="SWL"]; nnn -- nnl [label="1"]; nnl -- nnn [label="2"]; nnl -- snl [label="3"]; snl -- snl [label="4"]; snl -- snn [label="5"]; snn -- snl [label="6"]; snn -- nwn [label="7"]; nwn -- nnl [label="8"]; snn -- swn [label="9"]; swn -- snl [label="10"]; swn -- swl [label="11"]; swl -- swn [label="12"]; swl -- swl [label="13"]; nwn -- nwl [label="14"]; nwl -- nwn [label="15"]; nwl -- swl [label="16"]; nwl -- snl [label="17"]; swl -- snl [label="18"];}