前言
在學習作業系統的時候,我們應該都學習過臨界區、互斥鎖這些概念,用于在并發環境下保證狀态的正确性。比如在秒殺時,100 個使用者同時搶 10 個電腦,為了避免少賣或者超賣,就需要使用鎖來進行并發控制。 在 Go語言 裡面互斥鎖是
sync.Mutex
,我們本篇文章就來學習下為什麼要使用互斥鎖、如何使用互斥鎖,以及使用時的常見問題。
為什麼要使用互斥鎖
我們來看一個示例:我們起了
10000
個協程将變量
num
加1,是以肯定會存在并發,如果我們不控制并發,10000 個協程都執行完後,該變量的值很大機率不等于 10000。
那麼為什麼會出現這個問題呢,原因是
num++
不是原子操作,它會先讀取變量
num
目前值,然後對這個值
加1
,再把結果儲存到
num
中。例如
10
個
goroutine
同時運作到
num++
這一行,可能同時讀取
num=1000
,都
加1
後再儲存,
num=1001
,這就與想要的結果不符。
package main
import (
"fmt"
"sync"
)
func main() {
num := 0
var wg sync.WaitGroup
threadCount := 10000
wg.Add(threadCount)
for i := 0; i < threadCount; i++ {
go func() {
defer wg.Done()
num++
}()
}
wg.Wait() // 等待 10000 個協程都執行完
fmt.Println(num) // 9388(每次都可能不一樣)
我們如果使用了互斥鎖,可以保證每次進入臨界區的隻有一個
goroutine
,一個
goroutine
執行完後,另一個
goroutine
才能進入臨界區執行,最終就實作了并發控制。
package main
import (
"fmt"
"sync"
)
func main() {
num := 0
var mutex sync.Mutex // 互斥鎖
var wg sync.WaitGroup
threadCount := 10000
wg.Add(threadCount)
for i := 0; i < threadCount; i++ {
go func() {
defer wg.Done()
mutex.Lock() // 加鎖
num++ // 臨界區
mutex.Unlock() // 解鎖
}()
}
wg.Wait()
fmt.Println(num) // 10000
如何使用互斥鎖
Mutex
保持
Go
一貫的簡潔風格,開箱即用,聲明一個變量預設是沒有加鎖的,加鎖使用
Lock()
方法,解鎖使用
Unlock()
方法。
使用方式一:直接聲明使用
這個在上例中已經展現了,直接看上面的例子就好
使用方式二:封裝在其他結構體中
我們可以将
Mutex
封裝在
struct
中,封裝成線程安全的函數供外部調用。比如我們封裝了一個線程安全的計數器,調用
Add()
就加一,調用
Count()
傳回計數器的值。
package main
import (
"fmt"
"sync"
)
type Counter struct {
num int
mutex sync.Mutex
}
// 加一操作,涉及到臨界區 num,加鎖解鎖
func (counter *Counter) Add() {
counter.mutex.Lock()
defer counter.mutex.Unlock()
counter.num++
}
// 傳回數量,涉及到臨界區 num,加鎖解鎖
func (counter *Counter) Count() int {
counter.mutex.Lock()
defer counter.mutex.Unlock()
return counter.num
}
func main() {
threadCount := 10000
var counter Counter
var wg sync.WaitGroup
wg.Add(threadCount)
for i := 0; i < threadCount; i++ {
go func() {
defer wg.Done()
counter.Add()
}()
}
wg.Wait() // 等待所有 goroutine 都執行完
fmt.Println(counter.Count()) // 10000
在
Go
中,
map
結構是不支援并發的,如果并發讀寫就會
panic
// 運作會 panic,提示 fatal error: concurrent map writes
func main() {
m := make(map[string]string)
var wait sync.WaitGroup
wait.Add(1000)
for i := 0; i < 1000; i++ {
item := fmt.Sprintf("%d", i)
go func()
基于
Mutex
,我們可以實作一個線程安全的
map
:
import (
"fmt"
"sync"
)
type ConcurrentMap struct {
mutex sync.Mutex
items map[string]interface{}
}
func (c *ConcurrentMap) Add(key string, value interface{}) {
c.mutex.Lock()
defer c.mutex.Unlock()
c.items[key] = value
}
func (c *ConcurrentMap) Remove(key string) {
c.mutex.Lock()
defer c.mutex.Unlock()
delete(c.items, key)
}
func (c *ConcurrentMap) Get(key string) interface{} {
c.mutex.Lock()
defer c.mutex.Unlock()
return c.items[key]
}
func NewConcurrentMap() ConcurrentMap {
return ConcurrentMap{
items: make(map[string]interface{}),
}
}
func main() {
m := NewConcurrentMap()
var wait sync.WaitGroup
wait.Add(1000)
for i := 0; i < 1000; i++ {
item := fmt.Sprintf("%d", i)
go func() {
wait.Done()
m.Add(item, item)
}()
}
wait.Wait()
fmt.Println(m.Get("100")) // 100
當然,基于互斥鎖
Mutex
實作的線程安全
map
并不是性能最好的,基于讀寫鎖
sync.RWMutex
和 分片 可以實作性能更好的、線程安全的
map
,開發中比較常用的并發安全
map
是 orcaman / concurrent-map。
互斥鎖的常見問題
從上面可以看出,
Mutex
的使用過程方法比較簡單,但還是有幾點需要注意:
-
是可以在 Mutex
中加鎖,在 goroutine A
中解鎖的,但是在實際使用中,盡量保證在同一個 goroutine 中加解鎖。比如 goroutine A 申請到了鎖,在處理臨界區資源的時候,goroutine B 把鎖釋放了,但是 A 以為自己還持有鎖,會繼續處理臨界區資源,就可能會出現問題。goroutine B
-
的加鎖解鎖基本都是成對出現,為了解決忘記解鎖,可以使用 Mutex
語句,在加鎖後直接 defer
;但是如果處理完臨界區資源後還有很多耗時操作,為了盡早釋放鎖,不建議使用 defer mutex.Unlock()
,而是在處理完臨界區資源後就調用 defer
盡早釋放鎖。mutex.Unlock()
// 邏輯複雜,可能會忘記釋放鎖
func main() {
var mutex sync.Mutex
mutex.Lock()
if *** {
if *** {
// 處理臨界區資源
mutex.Unlock()
return
}
// 處理臨界區資源
mutex.Unlock()
return
}
// 處理臨界區資源
mutex.Unlock()
return
}
// 避免邏輯複雜忘記釋放鎖,使用 defer語句,成對出現
func main() {
var mutex sync.Mutex
mutex.Lock()
defer mutex.Unlock()
if *** {
if *** {
// 處理臨界區資源
return
}
// 處理臨界區資源
return
}
// 處理臨界區資源
return
- Mutex 不能複制使用
Mutex
是有狀态的,比如我們對一個
Mutex
加鎖後,再進行複制操作,會把目前的加鎖狀态也給複制過去,基于加鎖的
Mutex
再加鎖肯定不會成功。進行複制操作可能聽起來是一個比較低級的錯誤,但是無意間可能就會犯這種錯誤。
package main
import (
"fmt"
"sync"
)
type Counter struct {
mutex sync.Mutex
num int
}
func SomeFunc(c Counter) {
c.mutex.Lock()
defer c.mutex.Unlock()
c.num--
}
func main() {
var counter Counter
counter.mutex.Lock()
defer counter.mutex.Unlock()
counter.num++
// Go都是值傳遞,這裡複制了 counter,此時 counter.mutex 是加鎖狀态,在 SomeFunc 無法再次加鎖,就會一直等待