首頁 > 軟體

Golang的鎖機制與使用技巧小結

2022-06-01 18:00:29

1. sync.Mutex詳解

sync.Mutex是Go中的互斥鎖,通過.lock()方法上鎖,.unlock()方法解鎖。需要注意的是,因為Go函數值傳遞的特點,sync.Mutex通過函數傳遞時,會進行一次拷貝,所以傳遞過去的鎖是一把全新的鎖,大家在使用時要注意這一點,另外sync.Mutex是非重入鎖,這一點要與Java中的鎖區分。

type Mutex {
    state int32
    sema  uint32
}

上面資料結構中的state最低三位分別表示 mutexLocked、mutexWoken 和 mutexStarving,剩下的位置用來表示當前有多少個 Goroutine 等待互斥鎖的釋放:

32                                               3             2             1             0 
 |                                               |             |             |             | 
 |                                               |             |             |             | 
 v-----------------------------------------------v-------------v-------------v-------------+ 
 |                                               |             |             |             v 
 |                 waitersCount                  |mutexStarving| mutexWoken  | mutexLocked | 
 |                                               |             |             |             | 
 +-----------------------------------------------+-------------+-------------+-------------+                                                                                                              
  • mutexLocked — 表示互斥鎖的鎖定狀態;
  • mutexWoken — 表示從正常模式被從喚醒;
  • mutexStarving — 當前的互斥鎖進入飢餓狀態;
  • waitersCount — 當前互斥鎖上等待的 goroutine 個數;

2. RWMutex詳解

type RWMutex struct {
	w           Mutex  // 複用互斥鎖
	writerSem   uint32 // 寫鎖監聽讀鎖釋放的號誌
	readerSem   uint32 // 讀鎖監聽寫鎖釋放的號誌
	readerCount int32  // 當前正在執行讀操作的數量
	readerWait  int32  // 當寫操作被阻塞時,需要等待讀操作完成的個數
}
  • 讀操作如何防止並行讀寫問題的?

RLock(): 申請讀鎖,每次執行此函數後,會對readerCount++,此時當有寫操作執行Lock()時會判斷readerCount>0,就會阻塞。

RUnLock(): 解除讀鎖,執行readerCount–,釋放號誌喚醒等待寫操作的goroutine。

  • 寫操作如何防止並行讀寫、並行寫寫問題?

Lock(): 申請寫鎖,獲取互斥鎖,此時會阻塞其他的寫操作。並將readerCount 置為 -1,當有讀操作進來,發現readerCount = -1, 即知道有寫操作在進行,阻塞。

Unlock(): 解除寫鎖,會先通知所有阻塞的讀操作goroutine,然後才會釋放持有的互斥鎖。

  • 寫操作的飢餓問題?

這是由於寫操作要等待讀操作結束後才可以獲得鎖,而寫操作在等待期間可能還有新的讀操作持續到來,如果寫操作等待所有讀操作結束,很可能會一直阻塞,這種現象稱之為寫操作被餓死。

通過RWMutex結構體中的readerWait屬性可完美解決這個問題。

當寫操作到來時,會把RWMutex.readerCount值拷貝到RWMutex.readerWait中,用於標記排在寫操作前面的讀者個數。

前面的讀操作結束後,除了會遞減RWMutex.readerCount,還會遞減RWMutex.readerWait值,當RWMutex.readerWait值變為0時喚醒寫操作。

3. sync.Map詳解

一般情況下解決並行讀寫 map 的思路是加一把大鎖,或者把一個 map 分成若干個小 map,對 key 進行雜湊,只操作相應的小 map。前者鎖的粒度比較大,影響效率;後者實現起來比較複雜,容易出錯。

而使用 sync.map 之後,對 map 的讀寫,不需要加鎖。並且它通過空間換時間的方式,使用 read 和 dirty 兩個 map 來進行讀寫分離,降低鎖時間來提高效率。

type Map struct {
	mu Mutex
	read atomic.Value // readOnly
	dirty map[interface{}]*entry
	misses int
}

// readOnly is an immutable struct stored atomically in the Map.read field.
type readOnly struct {
	m       map[interface{}]*entry
	amended bool // true if the dirty map contains some key not in m.
}

type entry struct {
	p unsafe.Pointer // *interface{}
}

在進行讀操作的時候,會先在read中找,沒有命中的話會鎖住dirty並且尋找,如果找到了miss計數+1,超過閾值時將dirty賦值給read;

在進行新增操作時,直接在dirty中新增;

在進行修改操作時,先改read,再改dirty;

在進行刪除操作時,將read中加上amended標記,dirty中直接刪除。

4. 原子操作 atomic.Value

願此操作的底層是靠 MESI 快取一致性協定來維持的。

Go的 atomic.Value 需要注意應該放入唯讀物件。

//atomic.Value原始碼

type Value struct {
	v interface{} // 所以可以儲存任何型別的資料
}

// 空 interface{} 的內部表示格式,作用是將interface{}型別分解,得到其中兩個欄位
type ifaceWords struct {
	typ  unsafe.Pointer
	data unsafe.Pointer
}

// 取資料就是正常走流程
func (v *Value) Load() (x interface{}) {
	vp := (*ifaceWords)(unsafe.Pointer(v))
	typ := LoadPointer(&vp.typ)
	if typ == nil || uintptr(typ) == ^uintptr(0) {
		// 第一次還沒寫入
		return nil
	}
  // 構造新的interface{}返回出去
	data := LoadPointer(&vp.data)
	xp := (*ifaceWords)(unsafe.Pointer(&x))
	xp.typ = typ
	xp.data = data
	return
}

// 寫資料(如何保證資料完整性)
func (v *Value) Store(x interface{}) {
	if x == nil {
		panic("sync/atomic: store of nil value into Value")
	}
  // 繞過 Go 語言型別系統的檢查,與任意的指標型別互相轉換
	vp := (*ifaceWords)(unsafe.Pointer(v)) // 舊值
	xp := (*ifaceWords)(unsafe.Pointer(&x)) // 新值
	for { // 配合CompareAndSwap達到樂觀鎖的功效
		typ := LoadPointer(&vp.typ)
		if typ == nil { // 第一次寫入
			runtime_procPin() // 禁止搶佔
			if !CompareAndSwapPointer(&vp.typ, nil, unsafe.Pointer(^uintptr(0))) {
				runtime_procUnpin() // 沒有搶到鎖,說明已經有別的執行緒搶先完成賦值,重新進入迴圈
				continue
			}
			// 首次賦值
			StorePointer(&vp.data, xp.data)
			StorePointer(&vp.typ, xp.typ)
			runtime_procUnpin() // 寫入成功,解除佔用狀態
			return
		}
		if uintptr(typ) == ^uintptr(0) {
			// 第一次寫入還未完成,繼續等待
			continue
		}
		// 兩次需要寫入相同型別
		if typ != xp.typ {
			panic("sync/atomic: store of inconsistently typed value into Value")
		}
		StorePointer(&vp.data, xp.data)
		return
	}
}

// 禁止搶佔,標記當前G在M上不會被搶佔,並返回當前所在P的ID。
func runtime_procPin()
// 解除G的禁止搶佔狀態,之後G可被搶佔。
func runtime_procUnpin()

5. 使用小技巧

  • 減小臨界區域(減少鎖的持有時間)
var m sync.Mutex

func DoSth() {
    // do sth1
    func() {
       u.lock()
       defer m.unlock()
       // do sth2
    }() 
    // do sth3
}

如上所示,如果do sth3中是很費時的io操作,使用這個技巧可以將臨界區減小,提高效能,不過,如果本身臨界區就不大,鎖操作後續沒有什麼費時操作,那麼也就沒有必要這樣操作了。

  • 減小鎖的粒度

在高並行場景下,用鎖的數量來換取並行效率,類似於java中ConcurrentHashmap的分段鎖思想,增加鎖的數量,減少一把鎖控制的資料量。

  • 讀寫分離(讀寫鎖): RWMutex,sync.Map

在讀多寫少的情景下,可以使用讀寫鎖,提高讀操作的並行效能。

  • 使用原子操作

原子操作是CPU指令級的操作,不會觸發g排程機制。,不阻塞執行流

到此這篇關於Golang的鎖機制與使用技巧精選的文章就介紹到這了,更多相關Golang 鎖機制內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com