1. 程式人生 > 其它 >【Glolang】 關於Go語言中的鎖

【Glolang】 關於Go語言中的鎖

在 Golang 裡有專門的方法來實現鎖,就是 sync 包,這個包有兩個很重要的鎖型別

一個叫Mutex, 利用它可以實現互斥鎖。一個叫RWMutex,利用它可以實現讀寫鎖。

特別說明:

  • sync.Mutex的鎖是不可以巢狀使用的。
  • sync.RWMutexRLock()是可以巢狀使用的。
  • sync.RWMutexmu.Lock()是不可以巢狀的。
  • sync.RWMutexmu.Lock()中不可以巢狀mu.RLock()

否則,會 panicfatal 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 鎖的兩種定義方法

// 第一種
var lock *sync.Mutex
lock = new(sync.Mutex)

// 第二種
lock := &sync.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{}
    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 鎖,同樣有兩種方法

// 第一種
var lock *sync.RWMutex
lock = new(sync.RWMutex)

// 第二種
lock := &sync.RWMutex{}

RWMutex 裡提供了兩種鎖,每種鎖分別對應兩個方法,為了避免死鎖,兩個方法應成對出現,必要時請使用 defer。

  • 讀鎖:呼叫 RLock 方法開啟鎖,呼叫 RUnlock 釋放鎖

  • 寫鎖:呼叫 Lock 方法開啟鎖,呼叫 Unlock 釋放鎖(和 Mutex類似)

接下來,直接看一下例子吧

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    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\[email protected]\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\[email protected]\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\[email protected]\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\[email protected]\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\[email protected]\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一個協程來加入定時器判斷是否鎖超時。

如果您覺得本文對您的學習有所幫助,可通過支付寶(左) 或者 微信(右) 來打賞博主,增加博主的寫作動力