Go的sync/mutex實現

概述

sync/mutex是Go語言底層基礎對象之一,用於構建多個goroutine間的同步邏輯,所以被大量高層對象所使用。
其工做模型相似於Linux內核的futex對象,具體實現極爲簡潔,性能也有保證。node

數據結構

type Mutex struct {
    state int32    
    sema  uint32   
}

mutex對象僅有兩個數值字段,分爲爲state(存儲狀態)和sema(用於計算休眠goroutine數量的信號量)。
初始化時填入的0值將mutex設定在未鎖定狀態,同時保證時間開銷最小。
這一特性容許將mutex做爲其它對象的子對象使用。segmentfault

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調用,但須要注意一下幾點:性能

  1. 同一個goroutine不能在「連續」調用Lock()函數兩次,不然會致使死鎖;
  2. 在確保調用順序正確的前提下,徹底能夠在一個goroutine中調用Lock(),在另外一箇中調用Unlock();
  3. 同一個mutex不能被「連續」調用Unlock()函數兩次,不然會致使異常。

狀態遷移

狀態列表

state的三個狀態段一共有以下狀態組合(-表示0):ui

狀態組合 休眠者 喚醒事件 鎖定
--- - - -
--L - -
-WL -
S-L -
SW- -
S-- - -
-W- - -
SWL

結合同步點分析,能夠獲得以下規則:
1. 非喚醒goroutine在嘗試鎖定時不消耗喚醒事件,對該狀態段不作改變;
2. 已喚醒goroutine必定會消耗喚醒事件,將重置該狀態段;
3. 解除鎖定後才嘗試發送喚醒事件。atom

遷移路徑

只有一個goroutine調用的狀況

請輸入圖片描述

這種狀況下,狀態只在-----L之間遷移(路徑1和2),且只涉及到FG和D兩個同步點。spa

有兩個goroutine調用的狀況

請輸入圖片描述

這種狀況下,狀態涉及-----LS-LS---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調用的狀況

請輸入圖片描述

這種狀況是兩個goroutine調用狀況的延續,在原基礎上再涉及-WLSWL兩個狀態。
假設三個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"];
}
相關文章
相關標籤/搜索