golang讀寫鎖實現與核心原理分析

讀寫鎖的特點

  讀寫鎖區別與互斥鎖的主要區別就是讀鎖之間是共享的,多個goroutine可以同時加讀鎖,但是寫鎖與寫鎖、寫鎖與讀鎖之間則是互斥的

寫鎖飢餓問題

  因爲讀鎖是共享的,所以如果當前已經有讀鎖,那後續goroutine繼續加讀鎖正常情況下是可以加鎖成功,但是如果一直有讀鎖進行加鎖,那嘗試加寫鎖的goroutine則可能會長期獲取不到鎖,這就是因爲讀鎖而導致的寫鎖飢餓問題

golang的讀寫鎖的實現

結構體定義

type RWMutex struct {
    w           Mutex  // held if there are pending writers
    writerSem   uint32 // 用於writer等待讀完成排隊的信號量
    readerSem   uint32 // 用於reader等待寫完成排隊的信號量
    readerCount int32  // 讀鎖的計數器
    readerWait  int32  // 等待讀鎖釋放的數量
}

讀鎖加鎖過程

在這裏插入圖片描述

func (rw *RWMutex) RLock() {
    if race.Enabled {
        _ = rw.w.state
        race.Disable()
    }
    // 累加reader計數器,如果小於0則表明有writer正在等待
    if atomic.AddInt32(&rw.readerCount, 1) < 0 {
        // 當前有writer正在等待讀鎖,讀鎖就加入排隊
        runtime_SemacquireMutex(&rw.readerSem, false)
    }
    if race.Enabled {
        race.Enable()
        race.Acquire(unsafe.Pointer(&rw.readerSem))
    }
}

讀鎖釋放邏輯

在這裏插入圖片描述

func (rw *RWMutex) RUnlock() {
    if race.Enabled {
        _ = rw.w.state
        race.ReleaseMerge(unsafe.Pointer(&rw.writerSem))
        race.Disable()
    }
    // 如果小於0,則表明當前有writer正在等待
    if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 {
        if r+1 == 0 || r+1 == -rwmutexMaxReaders {
            race.Enable()
            throw("sync: RUnlock of unlocked RWMutex")
        }
        // 將等待reader的計數減1,證明當前是已經有一個讀的,如果值==0,則進行喚醒等待的
        if atomic.AddInt32(&rw.readerWait, -1) == 0 {
            // The last reader unblocks the writer.
            runtime_Semrelease(&rw.writerSem, false)
        }
    }
    if race.Enabled {
        race.Enable()
    }
}

寫鎖實現

加寫鎖實現

在這裏插入圖片描述

func (rw *RWMutex) Lock() {
    if race.Enabled {
        _ = rw.w.state
        race.Disable()
    }
    // 首先獲取mutex鎖,同時多個goroutine只有一個可以進入到下面的邏輯
    rw.w.Lock()
    // 對readerCounter進行進行搶佔,通過遞減rwmutexMaxReaders允許最大讀的數量
    // 來實現寫鎖對讀鎖的搶佔
    r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders
    // 記錄需要等待多少個reader完成,如果發現不爲0,則表明當前有reader正在讀取,當前goroutine
    // 需要進行排隊等待
    if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
        runtime_SemacquireMutex(&rw.writerSem, false)
    }
    if race.Enabled {
        race.Enable()
        race.Acquire(unsafe.Pointer(&rw.readerSem))
        race.Acquire(unsafe.Pointer(&rw.writerSem))
    }
}

釋放寫鎖在這裏插入圖片描述

func (rw *RWMutex) Unlock() {
    if race.Enabled {
        _ = rw.w.state
        race.Release(unsafe.Pointer(&rw.readerSem))
        race.Disable()
    }

    // 將reader計數器復位,上面減去了一個rwmutexMaxReaders現在再重新加回去即可復位
    r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)
    if r >= rwmutexMaxReaders {
        race.Enable()
        throw("sync: Unlock of unlocked RWMutex")
    }
    // 喚醒所有的讀鎖
    for i := 0; i < int(r); i++ {
        runtime_Semrelease(&rw.readerSem, false)
    }
    // 釋放mutex
    rw.w.Unlock()
    if race.Enabled {
        race.Enable()
    }
}

關鍵核心機制

寫鎖對讀鎖的搶佔

  加寫鎖的搶佔

	// 在加寫鎖的時候通過將readerCount遞減最大允許加讀鎖的數量,來實現對加讀鎖的搶佔
	r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders

  加讀鎖的搶佔檢測

// 如果沒有寫鎖的情況下讀鎖的readerCount進行Add後一定是一個>0的數字,這裏通過檢測值爲負數
//就實現了讀鎖對寫鎖搶佔的檢測
	if atomic.AddInt32(&rw.readerCount, 1) < 0 {
        // A writer is pending, wait for it.
        runtime_SemacquireMutex(&rw.readerSem, false)
    }

  寫鎖搶佔讀鎖後後續的讀鎖就會加鎖失敗,但是如果想加寫鎖成功還要繼續對已經加讀鎖成功的進行等待

if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
    // 寫鎖發現需要等待的讀鎖釋放的數量不爲0,就自己自己去休眠了
    runtime_SemacquireMutex(&rw.writerSem, false)
}

  寫鎖既然休眠了,則必定要有一種喚醒機制其實就是每次釋放鎖的時候,當檢查到有加寫鎖的情況下,就遞減readerWait,並由最後一個釋放reader lock的goroutine來實現喚醒寫鎖

if atomic.AddInt32(&rw.readerWait, -1) == 0 {
    // The last reader unblocks the writer.
    runtime_Semrelease(&rw.writerSem, false)
}

寫鎖的公平性

  在加寫鎖的時候必須先進行mutex的加鎖,而mutex本身在普通模式下是非公平的,只有在飢餓模式下才是公平的

rw.w.Lock()

寫鎖與讀鎖的公平性

  在加讀鎖和寫鎖的工程中都使用atomic.AddInt32來進行遞增,而該指令在底層是會通過LOCK來進行CPU總線加鎖的,因此多個CPU同時執行readerCount其實只會有一個成功,從這上面看其實是寫鎖與讀鎖之間是相對公平的,誰先達到誰先被CPU調度執行,進行LOCK鎖cache line成功,誰就加成功鎖

可見性與原子性問題

  在併發場景中特別是JAVA中通常會提到併發裏面的兩個問題:可見性與內存屏障、原子性, 其中可見性通常是指在cpu多級緩存下如何保證緩存的一致性,即在一個CPU上修改了了某個數據在其他的CPU上不會繼續讀取舊的數據,內存屏障通常是爲了CPU爲了提高流水線性能,而對指令進行重排序而來,而原子性則是指的執行某個操作的過程的不可分割

底層實現的CPU指令

  go裏面並沒有volatile這種關鍵字,那如何能保證上面的AddInt32這個操作可以滿足上面的兩個問題呢, 其實關鍵就在於底層的2條指令,通過LOCK指令配合CPU的MESI協議,實現可見性和內存屏障,同時通過XADDL則用來保證原子性,從而解決上面提到的可見性與原子性問題

// atomic/asm_amd64.s TEXT runtime∕internal∕atomic·Xadd(SB)
LOCK
XADDL   AX, 0(BX)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章