Golang中Mutex的實現

一.Golang中的鎖

       Golang的提供的同步機制有sync模塊下的Mutex、WaitGroup以及語言自身提供的chan等。 這些同步的方法都是以runtime中實現的底層同步機制(cas、atomic、spinlock、sem)爲基礎的。

1 cas(Compare And Swap)和原子運算是其他同步機制的基礎

  • 原子操作:指那些不能夠被打斷的操作被稱爲原子操作,當有一個CPU在訪問這塊內容addr時,其他CPU就不能訪問。
  • CAS:比較及交換,其實也屬於原子操作,但它是非阻塞的,所以在被操作值被頻繁變更的情況下,CAS操作並不那麼容易成功,不得不利用for循環以進行多次嘗試。

2 自旋鎖(spinlock)

       自旋鎖是指當一個線程在獲取鎖的時候,如果鎖已經被其他線程獲取,那麼該線程將循環等待,然後不斷地判斷是否能夠被成功獲取,知直到獲取到鎖纔會退出循環。獲取鎖的線程一直處於活躍狀態 Golang中的自旋鎖用來實現其他類型的鎖,與互斥鎖類似,不同點在於,它不是通過休眠來使進程阻塞,而是在獲得鎖之前一直處於活躍狀態(自旋)。

3 信號量

       實現休眠和喚醒協程的一種方式。

信號量有兩個操作P和V
P(S):分配一個資源
1. 資源數減1:S=S-1
2. 進行以下判斷
    如果S<0,進入阻塞隊列等待被釋放
    如果S>=0,直接返回

V(S):釋放一個資源
1. 資源數加1:S=S+1
2. 進行如下判斷
    如果S>0,直接返回
    如果S<=0,表示還有進程在請求資源,釋放阻塞隊列中的第一個等待進程
    
golang中信號量操作:runtime/sema.go
P操作:runtime_Semacquire
V操作:runtime_Semrelease

二 Mutex的實現

type Mutex struct {
	state int32     //0(可用) 1(被鎖) 2~31等待隊列計數
	sema  uint32    //信號量,向處於Gwaitting的G發送信號
}

const (
	mutexLocked = 1 << iota // 1 互斥鎖是鎖定的
	mutexWoken              // 2 喚醒鎖
	mutexWaiterShift = iota // 2 統計阻塞在這個互斥鎖上的goroutine數目需要移位的數值
)

2.1 互斥鎖

Lock方法申請對mutex加鎖的時候分爲兩種情況:

  • 有衝突:通過SAS把當前狀態設置爲加鎖狀態。
  • 無衝突:通過調用semacquire函數進入休眠狀態,等待其他協程釋放鎖的時候喚醒。
//如果已經加鎖,那麼當前協程進入休眠阻塞,等待喚醒
func (m *Mutex) Lock() {

    // 快速加鎖:CAS更新state爲locked
    if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
        return
    }

    awoke := false //當前goroutine是否被喚醒
    for {
        old := m.state // 保存當前state的狀態
        new := old | mutexLocked // 新值locked位設置爲1
        // 如果當前處於加鎖狀態,新到來的goroutine進入等待隊列
        if old&mutexLocked != 0 {
            new = old + 1<<mutexWaiterShift
        }
        if awoke {
            //如果被喚醒,新值需要重置woken位爲 0
            new &^= mutexWoken
        }
        
        // 兩種情況會走到這裏:1.休眠中被喚醒 2.加鎖失敗進入等待隊列
        // CAS 更新,如果更新失敗,說明有別的協程搶先一步,那麼重新發起競爭。
        if atomic.CompareAndSwapInt32(&m.state, old, new) {
            // 如果更新成功,有兩種情況
            // 1.如果爲 1,說明當前 CAS 是爲了更新 waiter 計數
            // 2.如果爲 0,說明是搶鎖成功,那麼直接 break 退出。
            if old&mutexLocked == 0 { 
                break
            }
            runtime_Semacquire(&m.sema) // 此時如果 sema <= 0 那麼阻塞在這裏等待喚醒,也就是 park 住。走到這裏都是要休眠了。
            awoke = true  // 有人釋放了鎖,然後當前 goroutine 被 runtime 喚醒了,設置 awoke true
        }
    }

    if raceenabled {
        raceAcquire(unsafe.Pointer(m))
    }
}

UnLock 解鎖分兩步:

  • 解鎖,通過CAS操作把當前狀態設置爲解鎖狀態。
  • 喚醒休眠協程,CAS操作把當前狀態的waiter數減1,然後喚醒休眠goroutine。
//鎖沒有和某個特定的協程關聯,可以由一個協程lock,另一個協程unlock
func (m *Mutex) Unlock() {
    if raceenabled {
        _ = m.state
        raceRelease(unsafe.Pointer(m))
    }

    // CAS更新state的狀態爲locked ======注意:解鎖的瞬間可能會有新的協程到來並搶到鎖
    new := atomic.AddInt32(&m.state, -mutexLocked)
    // 釋放了一個沒上鎖的鎖會panic:原先的lock位爲0
    if (new+mutexLocked)&mutexLocked == 0 { 
        panic("sync: unlock of unlocked mutex")
    }
    
    //判斷是否需要釋放資源
    old := new
    for {
        /**
         * 不需要喚醒的情況
         * 1.等待隊列爲0
         * 2.已經有協程搶到鎖(上面的瞬間搶鎖)
         * 3.已經有協程被喚醒
         */
        if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken) != 0 {
            return
        }
        //將waiter計數位減一,並設置state爲woken(喚醒)
        //問:會同時有多個被喚醒的協程存在嗎
        new = (old - 1<<mutexWaiterShift) | mutexWoken
        if atomic.CompareAndSwapInt32(&m.state, old, new) {
            runtime_Semrelease(&m.sema) // cas成功後,再做sema release操作,喚醒休眠的 goroutine
            return
        }
        old = m.state
    }
}

一代互斥鎖的問題:

  • 處於休眠中的goroutine優先級低於當前活躍的,unlock解鎖的瞬間最新的goroutine會搶到鎖。
  • 大多數果鎖的時間很短,所有的goroutine都要休眠,增加runtime調度開銷。

2.2 自旋鎖

Lock 方法申請對 mutex 加鎖的時候分三種情況:

  1. 無衝突 通過 CAS 操作把當前狀態設置爲加鎖狀態。
  2. 有衝突 開始自旋,並等待鎖釋放,如果其他 goroutine 在這段時間內釋放了該鎖,直接獲得該鎖;如果沒有釋放,進入3。
  3. 有衝突 通過調用 semacquire 函數來讓當前 goroutine 進入等待狀態,等待其他協程釋放鎖的時候喚醒。
func (m *Mutex) Lock() {
    //快速加鎖,邏輯不變
    if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
        if race.Enabled {
            race.Acquire(unsafe.Pointer(m))
        }
        return
    }

    awoke := false
    iter := 0
    for {
        old := m.state
        new := old | mutexLocked
        if old&mutexLocked != 0 { // 如果當前己經上鎖,那麼判斷是否可以自旋
            //短暫的自旋過後如果無果,就只能通過信號量讓當前goroutine進入休眠等待了
            if runtime_canSpin(iter) {
                // Active spinning makes sense.
                /**
                 * 自旋的操作:設置state爲woken,這樣在unlock的時候就不會喚醒其他協程.
                 * 自旋的條件:
                 * 1.當前協程未被喚醒 !awoke
                 * 2.其他協程未被喚醒 old&mutexWoken == 0
                 * 3.等待隊列大於0
                 */
                if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
                    atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
                    awoke = true
                }
                //進行自旋操作
                runtime_doSpin()
                iter++
                continue
            }
            new = old + 1<<mutexWaiterShit
        }
        if awoke {
            //todo 爲什麼加這個判斷
            if new&mutexWoken == 0 {
                panic("sync: inconsistent mutex state")
            }
            new &^= mutexWoken
        }
        if atomic.CompareAndSwapInt32(&m.state, old, new) {
            if old&mutexLocked == 0 {
                break
            }
            runtime_Semacquire(&m.sema)
            awoke = true
            iter = 0
        }
    }

    if race.Enabled {
        race.Acquire(unsafe.Pointer(m))
    }
}

二代自旋鎖的問題:

  • 還是沒有解決休眠進程優先級低的問題

2.3 公平鎖

基本邏輯:

  1. Mutex 兩種工作模式,normal 正常模式,starvation 飢餓模式。normal 情況下鎖的邏輯與老版相似,休眠的 goroutine 以 FIFO 鏈表形式保存在 sudog 中,被喚醒的 goroutine 與新到來活躍的 goroutine 競解,但是很可能會失敗。如果一個 goroutine 等待超過 1ms,那麼 Mutex 進入飢餓模式。
  2. 飢餓模式下,解鎖後,鎖直接交給 waiter FIFO 鏈表的第一個,新來的活躍 goroutine 不參與競爭,並放到 FIFO 隊尾。
  3. 如果當前獲得鎖的 goroutine 是 FIFO 隊尾,或是等待時長小於 1ms,那麼退出飢餓模式。
  4. normal 模式下性能是比較好的,但是 starvation 模式能減小長尾 latency。

LOCK流程

  1. 無衝突: 通過 CAS 操作把當前狀態設置爲加鎖狀態。
  2. 有衝突,開始自旋: 如果是飢餓模式禁止自旋,開始自旋,並等待鎖釋放,如果其他 goroutine 在這段時間內釋放了該鎖,直接獲得該鎖;如果沒有釋放,進入3。
  3. 有衝突,且已經過了自旋階段 :通過調用 semacquire 函數來讓當前 goroutine 進入等待狀態,等待其他協程釋放鎖的時候喚醒,休眠前:如果是飢餓模式,把當前協程放到隊列最前面;喚醒後:如果是飢餓模式喚醒的,直接獲得鎖。
type Mutex struct {
	state int32 
	sema  **uint32**
}

// A Locker represents an object that can be locked and unlocked.
type Locker interface {
	Lock()
	Unlock()
}

//爲什麼使用位掩碼錶達式
//第3位到第32位表示等待在mutex上協程數量
const (
	mutexLocked = 1 << iota // mutex is locked 
	mutexWoken                                  
	mutexStarving           //新增飢餓狀態
	mutexWaiterShift = iota                     
	starvationThresholdNs = 1e6 //飢餓狀態的閾值:等待時間超過1ms就會進入飢餓狀態
)


func (m *Mutex) Lock() {
	//快速加鎖:邏輯不變
	if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
		if race.Enabled {
			race.Acquire(unsafe.Pointer(m))
		}
		return
	}

	var waitStartTime int64 //等待時間
	starving := false   //飢餓標記
	awoke := false      //喚醒標記
	iter := 0           //循環計數器
	old := m.state      //保存當前鎖狀態
	for {
	    // 自旋的時候增加了一個判斷:如果處於飢餓狀態就不進入自旋,因爲飢餓模式下,釋放的鎖會直接給等待隊列的第一個,當前協程直接進入等待隊列
		if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
			if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
				atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
				awoke = true
			}
			runtime_doSpin()
			iter++
			old = m.state
			continue
		}
		new := old
		// 當mutex不處於飢餓狀態的時候,將new值設置爲locked,也就是說如果是飢餓狀態,新到來的goroutine直接排隊
		if old&mutexStarving == 0 {
			new |= mutexLocked
		}
		// 當mutex處於加鎖鎖或者飢餓狀態時,新到來的goroutine進入等待隊列
		if old&(mutexLocked|mutexStarving) != 0 {
			new += 1 << mutexWaiterShift
		}
		// 當等待時間超過閾值,當前goroutine切換mutex爲飢餓模式,如果未加鎖,就不需要切換
		if starving && old&mutexLocked != 0 {
			new |= mutexStarving
		}
		if awoke {
			if new&mutexWoken == 0 {
				throw("sync: inconsistent mutex state")
			}
			new &^= mutexWoken
		}
		if atomic.CompareAndSwapInt32(&m.state, old, new) {
		    // mutex 處於未加鎖,正常模式下,當前 goroutine 獲得鎖
			if old&(mutexLocked|mutexStarving) == 0 {
				break // locked the mutex with CAS
			}
			// 如果已經在排隊了,就排到隊伍的最前面
			queueLifo := waitStartTime != 0
			if waitStartTime == 0 {
				waitStartTime = runtime_nanotime()
			}
			// queueLifo 爲真的時候,當前goroutine會被放到隊頭,
			// 也就是說被喚醒卻沒搶到鎖的goroutine放到最前面
			runtime_SemacquireMutex(&m.sema, queueLifo)
			// 當前goroutine等待時間超過閾值,切換爲飢餓模式,starving設置爲true
			starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
			old = m.state
			//如果當前是飢餓模式
			if old&mutexStarving != 0 {
				if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {
					throw("sync: inconsistent mutex state")
				}
				// 如果切換爲飢餓模式,等待隊列計數減1
				delta := int32(mutexLocked - 1<<mutexWaiterShift)
				// 如果等待時間小於1ms或者自己是最後一個被喚醒的,退出飢餓模式
				if !starving || old>>mutexWaiterShift == 1 {
					delta -= mutexStarving
				}
				atomic.AddInt32(&m.state, delta)
				break
			}
			awoke = true
			iter = 0
		} else {
			old = m.state
		}
	}

	if race.Enabled {
		race.Acquire(unsafe.Pointer(m))
	}
}

UnLock 解鎖分兩步:

  1. 解鎖,通過CAS操作把當前狀態設置爲解鎖狀態。
  2. 喚醒休眠協程,CAS操作把當前狀態的waiter數減1,然後喚醒休眠goroutine,如果是飢餓模式的話,喚醒等待隊列的第一個。
func (m *Mutex) Unlock() {
	if race.Enabled {
		_ = m.state
		race.Release(unsafe.Pointer(m))
	}

	new := atomic.AddInt32(&m.state, -mutexLocked)
	if (new+mutexLocked)&mutexLocked == 0 {
		throw("sync: unlock of unlocked mutex")
	}
	
	if new&mutexStarving == 0 {
	// 正常模式
		old := new
		for {
		    /**
             * 不需要喚醒的情況
             * 1.等待隊列爲0
             * 2.已經有協程搶到鎖(上面的瞬間搶鎖)
             * 3.已經有協程被喚醒
             * 4.處於飢餓模式 在飢餓模式獲取到鎖的協程仍然處於飢餓狀態,新的goroutine無法獲取到鎖
             */
			if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
				return
			}
			// Grab the right to wake someone.
			new = (old - 1<<mutexWaiterShift) | mutexWoken
			if atomic.CompareAndSwapInt32(&m.state, old, new) {
				runtime_Semrelease(&m.sema, false)
				return
			}
			old = m.state
		}
	} else {
	    // 飢餓模式
		runtime_Semrelease(&m.sema, true)
	}
}

 

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