雖然Go語言提供channel來保證協程的通訊,但是某些場景用鎖來顯示保證協程的安全更清晰易懂。
Go語言中主要有兩種鎖,互斥鎖Mutex和讀寫鎖RWMutex,下面分別介紹一下使用方法,以及出現死鎖的常見場景。
一、Mutex(互斥鎖)
Mutex是互斥鎖的意思,也叫排他鎖,同一時刻一段程式碼只能被一個執行緒執行,使用只需要關注方法Lock(加鎖)和Unlock(解鎖)即可。
在Lock()和Unlock()之間的程式碼段稱為資源的臨界區(critical section),是執行緒安全的,任何一個時間點都只能有一個goroutine執行這段區間的程式碼。
不加鎖示例
先來一段不加群的程式碼,10個協程同時累加1萬
package main
import (
"fmt"
"sync"
)
func main() {
var count = 0
var wg sync.WaitGroup
//十個協程數量
n := 10
wg.Add(n)
for i := 0; i < n; i++ {
go func() {
defer wg.Done()
//1萬疊加
for j := 0; j < 10000; j++ {
count++
}
}()
}
wg.Wait()
fmt.Println(count)
}
執行結果如下
38532
正確的結果應該是100000,這裡出現了併發寫入更新錯誤的情況
加鎖示例
我們再新增鎖,程式碼如下
package main
import (
"fmt"
"sync"
)
func main() {
var count = 0
var wg sync.WaitGroup
var mu sync.Mutex
//十個協程數量
n := 10
wg.Add(n)
for i := 0; i < n; i++ {
go func() {
defer wg.Done()
//1萬疊加
for j := 0; j < 10000; j++ {
mu.Lock()
count++
mu.Unlock()
}
}()
}
wg.Wait()
fmt.Println(count)
}
執行結果如下,可以看到,已經看到結果變成了正確的100000
二、RWMutex(讀寫鎖)
Mutex在大量併發的情況下,會造成鎖等待,對效能的影響比較大。
如果某個讀操作的協程加了鎖,其他的協程沒必要處於等待狀態,可以併發地訪問共享變數,這樣能讓讀操作並行,提高讀效能。
RWLock就是用來幹這個的,這種鎖在某一時刻能由什麼問題數量的reader持有,或者被一個wrtier持有
主要遵循以下規則 :
- 讀寫鎖的讀鎖可以重入,在已經有讀鎖的情況下,可以任意加讀鎖。
- 在讀鎖沒有全部解鎖的情況下,寫操作會阻塞直到所有讀鎖解鎖。
- 寫鎖定的情況下,其他協程的讀寫都會被阻塞,直到寫鎖解鎖。
Go語言的讀寫鎖方法主要有下面這種
- Lock/Unlock:針對寫操作。
不管鎖是被reader還是writer持有,這個Lock方法會一直阻塞,Unlock用來釋放鎖的方法 - RLock/RUnlock:針對讀操作
當鎖被reader所有的時候,RLock會直接返回,當鎖已經被writer所有,RLock會一直阻塞,直到能獲取鎖,否則就直接返回,RUnlock用來釋放鎖的方法
併發讀示例
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var m sync.RWMutex
go read(&m, 1)
go read(&m, 2)
go read(&m, 3)
time.Sleep(2 * time.Second)
}
func read(m *sync.RWMutex, i int) {
fmt.Println(i, "reader start")
m.RLock()
fmt.Println(i, "reading")
time.Sleep(1 * time.Second)
m.RUnlock()
fmt.Println(i, "reader over")
}
執行如下
可以看到,3的讀還沒結束,1和2已經開始讀了
併發讀寫示例
package main
import (
"fmt"
"sync"
"time"
)
var count = 0
func main() {
var m sync.RWMutex
for i := 1; i <= 3; i++ {
go write(&m, i)
}
for i := 1; i <= 3; i++ {
go read(&m, i)
}
time.Sleep(1 * time.Second)
fmt.Println("final count:", count)
}
func read(m *sync.RWMutex, i int) {
fmt.Println(i, "reader start")
m.RLock()
fmt.Println(i, "reading count:", count)
time.Sleep(1 * time.Millisecond)
m.RUnlock()
fmt.Println(i, "reader over")
}
func write(m *sync.RWMutex, i int) {
fmt.Println(i, "writer start")
m.Lock()
count++
fmt.Println(i, "writing count", count)
time.Sleep(1 * time.Millisecond)
m.Unlock()
fmt.Println(i, "writer over")
}
執行結果如下
如果我們可以明確區分reader和writer的協程場景,且是大師的併發讀、少量的併發寫,有強烈的效能需要,我們就可以考慮使用讀寫鎖RWMutex替換Mutex
三、死鎖場景
當兩個或兩個以上的程式在執行過程中,因爭奪資源而處理一種互相等待的狀態,如果沒有外部干涉無法繼續下去,這時我們稱系統處於死鎖或產生了死鎖。
死鎖主要有以下幾種場景。
Lock/Unlock不是成對出現
沒有成對出現容易會出現死鎖的情況,或者是Unlock 一個未加鎖的Mutex而導致 panic,程式碼建議以下面緊湊的方式出現
mu.Lock()
defer mu.Unlock()
鎖被拷貝使用
package main
import (
"fmt"
"sync"
)
func main() {
var mu sync.Mutex
mu.Lock()
defer mu.Unlock()
copyTest(mu)
}
//這裡複製了一個鎖,造成了死鎖
func copyTest(mu sync.Mutex) {
mu.Lock()
defer mu.Unlock()
fmt.Println("ok")
}
在函式外層已經加了一個Lock,在拷貝的時候又執行了一次Lock,因此這是一個永遠不會獲得的鎖,因為外層函式的Unlock無法執行。
迴圈等待
A等待B,B等待C,C等待A,陷入了無限迴圈(哲學家就餐問題)
package main
import (
"sync"
)
func main() {
var muA, muB sync.Mutex
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
muA.Lock()
defer muA.Unlock()
//A依賴B
muB.Lock()
defer muB.Lock()
}()
go func() {
defer wg.Done()
muB.Lock()
defer muB.Lock()
//B依賴A
muA.Lock()
defer muA.Unlock()
}()
wg.Wait()
}
以上就是Go語言的鎖使用,由chenqionghe傾情整理,giao~