sync/mutex是Go語言底層基礎對象之一,用於構建多個goroutine間的同步邏輯,所以被大量高層對象所使用。
其工做模型相似於Linux內核的futex對象,具體實現極爲簡潔,性能也有保證。node
type Mutex struct { state int32 sema uint32 }
mutex對象僅有兩個數值字段,分爲爲state(存儲狀態)和sema(用於計算休眠goroutine數量的信號量)。
初始化時填入的0值將mutex設定在未鎖定狀態,同時保證時間開銷最小。
這一特性容許將mutex做爲其它對象的子對象使用。segmentfault
state被定義爲int32類型,容許爲其調用原子方法(sync/atomic),從而原子化地設定狀態。
每一個state字段均劃分三個狀態段,含義以下:數據結構
31 3 2 1 0 +----~~~----+-+-+-+ | S | |W|L| +----~~~----+-+-+-+ | | | | | | | \--- 鎖定狀態,0表示未鎖定,1表示已鎖定 | | | | | \----- 喚醒事件,0表示無事件,1表示mutex已被解除鎖定,能夠喚醒等待其它goroutine | | | \------- 保留位,保持爲0 | \------------------- 等待喚醒以嘗試鎖定的goroutine的計數,0表示沒有等待者
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)) } }
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調用,但須要注意一下幾點:性能
state的三個狀態段一共有以下狀態組合(-
表示0):ui
狀態組合 | 休眠者 | 喚醒事件 | 鎖定 |
---|---|---|---|
--- |
- | - | - |
--L |
- | - | 是 |
-WL |
- | 有 | 是 |
S-L |
有 | - | 是 |
SW- |
有 | 有 | - |
S-- |
有 | - | - |
-W- |
- | 有 | - |
SWL |
有 | 有 | 是 |
結合同步點分析,能夠獲得以下規則:
1. 非喚醒goroutine在嘗試鎖定時不消耗喚醒事件,對該狀態段不作改變;
2. 已喚醒goroutine必定會消耗喚醒事件,將重置該狀態段;
3. 解除鎖定後才嘗試發送喚醒事件。atom
這種狀況下,狀態只在---
與--L
之間遷移(路徑1和2),且只涉及到FG和D兩個同步點。spa
這種狀況下,狀態涉及---
、--L
、S-L
、S--
、-W-
、-WL
六個狀態。
假設兩個goroutine分別稱爲X和Y:code
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調用狀況的延續,在原基礎上再涉及-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"]; }