友情提示:此篇文章大約需要閱讀 5分鐘45秒,不足之處請多指教,感謝你的閱讀。 訂閱本站
我們比較常見的大型專案的設計中都會出現併發訪問問題,併發就是為了解決資料的準確性,保證同一個臨界區的資料只能被一個執行緒進行操作,日常中使用到的併發場景也是很多的:
- 計數器:計數器結果不準確;
- 秒殺系統:由於同一時間訪問量比較大,導致的超賣;
- 使用者賬戶異常:同一時間支付導致的賬戶透支;
- buffer 資料異常:更新 buffer 導致的資料混亂。
上面都是併發帶來的資料準確性的問題,決絕方案就是使用互斥鎖,也就是今天併發程式設計中的所要描述的 Mutex 併發原語。
實現機制
互斥鎖 Mutex 就是為了避免併發競爭建立的併發控制機制,其中有個“臨界區”的概念。
在併發程式設計過程中,如果程式中一部分資源或者變數會被併發訪問或者修改,為了避免併發訪問導致資料的不準確,這部分程式需要率先被保護起來,之後操作,操作結束後去除保護,這部分被保護的程式就叫做臨界區。
使用互斥鎖,限定臨界區只能同時由一個執行緒持有,若是臨界區此時被一個執行緒持有,那麼其他執行緒想進入到這個臨界區的時候,就會失敗或者等待釋放鎖,持有此臨界區的執行緒退出,其他執行緒才有機會獲得這個臨界區。
go mutex 臨界區示意圖
Mutex 是 Go 語言中使用最廣泛的同步原語,也稱為併發原語,解決的是併發讀寫共享資源,避免出現資料競爭 data race 問題。
基本使用
互斥鎖 Mutex 提供了兩個方法 Lock 和 Unlock:進入到臨界區使用 Lock 方法加鎖,退出臨界區使用 Unlock 方法釋放鎖 ?。
type Locker interface {
Lock()
Unlock()
}
func(m *Mutex)Lock()
func(m *Mutex)Unlock()
當一個 goroutine 呼叫 Lock 方法獲取到鎖後,其他 goroutine 會阻塞在 Lock 的呼叫上,直到當前獲取到鎖的 goroutine 釋放鎖。
接下來是一個計數器的例子,是由 100 個 goroutine 對計數器進行累加操作,最後輸出結果:
package main
import (
"fmt"
"sync"
)
func main() {
var mu sync.Mutex
countNum := 0
// 確認輔助變數是否都執行完成
var wg sync.WaitGroup
// wg 新增數目要和 建立的協程數量保持一致
wg.Add(100)
for i := 0; i < 100; i++ {
go func() {
defer wg.Done()
for j := 0; j < 1000; j++ {
mu.Lock()
countNum++
mu.Unlock()
}
}()
}
wg.Wait()
fmt.Printf("countNum: %d", countNum)
}
實際使用
很多時候 Mutex 並不是單獨使用的,而是巢狀在 Struct 中使用,作為結構體的一部分,如果嵌入的 struct 有多個欄位,我們一般會把 Mutex 放在要控制的欄位上面,然後使用空格把欄位分隔開來。
甚至可以把獲取鎖、釋放鎖、計數加一的邏輯封裝成一個方法。
package main
import (
"fmt"
"sync"
)
// 執行緒安全的計數器
type Counter struct {
CounterType int
Name string
mu sync.Mutex
count uint64
}
// 加一方法
func (c *Counter) Incr() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}
// 取數值方法 執行緒也需要受保護
func (c *Counter) Count() uint64 {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}
func main() {
// 定義一個計數器
var counter Counter
var wg sync.WaitGroup
wg.Add(100)
for i := 0; i < 100; i++ {
go func() {
defer wg.Done()
for j := 0; j < 1000; j++ {
counter.Incr()
}
}()
}
wg.Wait()
fmt.Printf("%d\n", counter.Count())
}
思考問題
Q:你已經知道,如果 Mutex 已經被一個 goroutine 獲取了鎖,其它等待中的 goroutine 們只能一直等待。那麼,等這個鎖釋放後,等待中的 goroutine 中哪一個會優先獲取 Mutex 呢?
A:FIFO,先來先服務的策略,Go 的 goroutine 排程中,會維護一個保障 goroutine 執行的佇列,當獲取到鎖的 goroutine 執行完臨界區的操作的時候,就會釋放鎖,在佇列中排在第一位置的 goroutine 會拿到鎖進行臨界區的操作。
本作品採用《CC 協議》,轉載必須註明作者和本文連結