在 Golang 里有專門的方法來實現鎖,就是 sync 包,這個包有兩個很重要的鎖類型
一個叫 Mutex, 利用它可以實現互斥鎖。一個叫 RWMutex,利用它可以實現讀寫鎖。
特別說明:
sync.Mutex的鎖是不可以嵌套使用的sync.RWMutex的RLock()是可以嵌套使用的sync.RWMutex的mu.Lock()是不可以嵌套的sync.RWMutex的mu.Lock()中不可以嵌套mu.RLock()
否則,會 panic fatal error: all goroutines are asleep - deadlock!
一、實例說明
package main
import (
"sync"
"time"
)
var l sync.RWMutex
func readAndRead() { // 可讀鎖內使用可讀鎖
l.RLock()
defer l.RUnlock()
l.RLock()
defer l.RUnlock()
}
func lockAndLock() { // 全局鎖內使用全局鎖
l.Lock()
defer l.Unlock()
l.Lock()
defer l.Unlock()
}
func lockAndRead() { // 全局鎖內使用可讀鎖
l.Lock()
defer l.Unlock() // 由於 defer 是棧式執行,所以這兩個鎖是嵌套結構
l.RLock()
defer l.RUnlock()
}
func readAndLock() { // 可讀鎖內使用全局鎖
l.RLock()
defer l.RUnlock()
l.Lock()
defer l.Unlock()
}
func main() {
readAndRead()
readAndLock()
lockAndRead()
lockAndLock()
time.Sleep(5 * time.Second)
}
二、 互斥鎖 :Mutex
使用互斥鎖(Mutex,全稱 mutual exclusion)是為了來保護一個資源不會因為並發操作而引起沖突導致數據不准確。下面這段代碼開啟了三個協程,每個協程分別往 count 這個變量加10000次 ,理論上看 count 值應試為 30000
package main
import (
"fmt"
"sync"
)
func add(count *int, wg *sync.WaitGroup) {
for i := 0; i < 10000; i++ {
*count = *count + 1
}
wg.Done()
}
func main() {
var wg sync.WaitGroup
count := 0
wg.Add(3)
go add(&count, &wg)
go add(&count, &wg)
go add(&count, &wg)
wg.Wait()
fmt.Println("count 的值為:", count)
}
執行的結果為:
PS E:\project\demo> go run test6.go count 的值為: 18186 PS E:\project\demo> go run test6.go count 的值為: 19154 PS E:\project\demo> go run test6.go count 的值為: 23215
原因就在於這三個協程在執行時,先讀取 count 再更新 count 的值,而這個過程並不具備原子性,所以導致了數據的不准確。解決這個問題的方法,就是給 add 這個函數加上 Mutex 互斥鎖,要求同一時刻,僅能有一個協程能對 count 操作。在寫代碼前,先了解一下 Mutex 鎖的兩種定義方法
然后修改代碼,如下所示
import (
"fmt"
"sync"
)
func add(count *int, wg *sync.WaitGroup, lock *sync.Mutex) {
for i := 0; i < 10000; i++ {
lock.Lock()
*count = *count + 1
lock.Unlock()
}
wg.Done()
}
func main() {
var wg sync.WaitGroup
lock := &sync.Mutex{}
//或者
//var lock *sync.Mutex
//lock = new(sync.Mutex)
count := 0
wg.Add(3)
go add(&count, &wg, lock)
go add(&count, &wg, lock)
go add(&count, &wg, lock)
wg.Wait()
fmt.Println("count 的值為:", count)
}
不管你執行多少次,輸出都只有一個結果,count 的值為: 30000
使用 Mutext 鎖雖然很簡單,但仍然有幾點需要注意:
-
同一協程里,不要在尚未解鎖時再次使加鎖
-
同一協程里,不要對已解鎖的鎖再次解鎖
-
加了鎖后,別忘了解鎖,必要時使用 defer 語句
三、讀寫鎖:RWMutex
Mutex 是最簡單的一種鎖類型,他提供了一個傻瓜式的操作,加鎖解鎖加鎖解鎖,讓你不需要再考慮其他的。簡單 同時意味着在某些特殊情況下有可能會造成時間上的浪費,導致程序性能低下。
-
為了保證數據的安全,它規定了當有人還在讀取數據(即讀鎖占用)時,不允計有人更新這個數據(即寫鎖會阻塞)
-
為了保證程序的效率,多個人(線程)讀取數據(擁有讀鎖)時,互不影響不會造成阻塞,它不會像 Mutex 那樣只允許有一個人(線程)讀取同一個數據。
理解了這個后,再來看看,如何使用 RWMutex?
定義一個 RWMuteux 鎖,同樣有兩種方法
RWMutex 里提供了兩種鎖,每種鎖分別對應兩個方法,為了避免死鎖,兩個方法應成對出現,必要時請使用 defer。
-
讀鎖:調用 RLock 方法開啟鎖,調用 RUnlock 釋放鎖
-
寫鎖:調用 Lock 方法開啟鎖,調用 Unlock 釋放鎖(和 Mutex類似)
接下來,直接看一下例子吧
package main
import (
"fmt"
"sync"
"time"
)
func main() {
//var lock *sync.RWMutex
//lock = new(sync.RWMutex)
//或者
lock := &sync.RWMutex{}
lock.Lock()
for i := 0; i < 4; i++ {
go func(i int) {
fmt.Printf("第 %d 個協程准備開始... \n", i)
lock.RLock()
fmt.Printf("第 %d 個協程獲得讀鎖, sleep 1s 后,釋放鎖\n", i)
time.Sleep(time.Second)
lock.RUnlock()
}(i)
}
time.Sleep(time.Second * 2)
fmt.Println("准備釋放寫鎖,讀鎖不再阻塞")
// 寫鎖一釋放,讀鎖就自由了
lock.Unlock()
// 由於會等到讀鎖全部釋放,才能獲得寫鎖
// 因為這里一定會在上面 4 個協程全部完成才能往下走
lock.Lock()
fmt.Println("程序退出...")
lock.Unlock()
}
執行結果如下
PS E:\project\demo> go run test8.go 第 0 個協程准備開始... 第 3 個協程准備開始... 第 1 個協程准備開始... 第 2 個協程准備開始... 准備釋放寫鎖,讀鎖不再阻塞 第 2 個協程獲得讀鎖, sleep 1s 后,釋放鎖 第 3 個協程獲得讀鎖, sleep 1s 后,釋放鎖 第 0 個協程獲得讀鎖, sleep 1s 后,釋放鎖 第 1 個協程獲得讀鎖, sleep 1s 后,釋放鎖 程序退出...
四、自動檢測死鎖deadlock
package main
import (
"fmt"
"sync"
"time"
"github.com/sasha-s/go-deadlock"
)
var (
mu1 deadlock.Mutex
mu2 deadlock.Mutex
wg sync.WaitGroup
)
func main() {
wg.Add(2)
go func() {
mu1.Lock()
time.Sleep(1 * time.Second)
mu2.Lock()
}()
go func() {
mu2.Lock()
mu1.Lock()
}()
go func() {
for {
time.Sleep(1 * time.Second)
fmt.Println("test")
}
}()
wg.Wait()
}
運行結果如下
POTENTIAL DEADLOCK: Inconsistent locking. saw this ordering in one goroutine:
test
happened before
..\pkg\mod\github.com\sasha-s\go-deadlock@v0.3.1\deadlock.go:85 go-deadlock.(*Mutex).Lock { lock(m.mu.Lock, m) } <<<<<
test9.go:26 main.main.func2 { mu2.Lock() }
happened after
..\pkg\mod\github.com\sasha-s\go-deadlock@v0.3.1\deadlock.go:85 go-deadlock.(*Mutex).Lock { lock(m.mu.Lock, m) } <<<<<
test9.go:27 main.main.func2 { mu1.Lock() }
in another goroutine: happened before
..\pkg\mod\github.com\sasha-s\go-deadlock@v0.3.1\deadlock.go:85 go-deadlock.(*Mutex).Lock { lock(m.mu.Lock, m) } <<<<<
test9.go:20 main.main.func1 { mu1.Lock() }
happened after
..\pkg\mod\github.com\sasha-s\go-deadlock@v0.3.1\deadlock.go:85 go-deadlock.(*Mutex).Lock { lock(m.mu.Lock, m) } <<<<<
test9.go:22 main.main.func1 { mu2.Lock() }
Other goroutines holding locks:
goroutine 19 lock 0x5d6ea8
..\pkg\mod\github.com\sasha-s\go-deadlock@v0.3.1\deadlock.go:85 go-deadlock.(*Mutex).Lock { lock(m.mu.Lock, m) } <<<<<
test9.go:20 main.main.func1 { mu1.Lock() }
exit status 2
在多場景下go-deadlock如何做的死鎖檢測 ?
場景1:當協程1拿到了lock1的鎖,然后再嘗試拿lock1鎖?
很簡單,用一個map存入所有為釋放鎖的協程id, 當檢測到gid相同時, 觸發OnPotentialDeadlock回調方法。如果拿到一個鎖,又通過 go func()去拿同樣的鎖,這時候就無法快速檢測死鎖了,只能依賴go-deadlock提供了鎖超時檢測。
場景2:協程1拿到了lock1, 協程2拿到了lock2, 這時候協程1再去拿lock2, 協程2嘗試去拿lock1
這是交叉拿鎖引起的死鎖問題,如何解決? 我們可以存入beferAfter關系。在go-deadlock里有個order map專門來存這個關系。當協程1再去拿lock2的時候, 如果order里有 lock1-lock2, 那么觸發OnPotentialDeadlock回調方法。
場景3:如果協程1拿到了lock1,但是沒有寫unlock方法,協程2嘗試拿lock1, 會一直阻塞的等待
go deadlock會針對開啟DeadlockTimeout >0 的加鎖過程,new一個協程來加入定時器判斷是否鎖超時。
