Golang 鎖的使用

鎖的介紹與使用

1 互斥鎖
傳統併發程序對共享資源進行訪問控制的主要手段,由標準庫代碼包中sync中的Mutex結構體表示。

//Mutex  是互斥鎖, 零值是解鎖的互斥鎖, 首次使用後不得複製互斥鎖。
type Mutex struct {
   state int32
   sema  uint32
}

sync.Mutex類型只有兩個公開的指針方法

//Locker表示可以鎖定和解鎖的對象。
type Locker interface {
   Lock()
   Unlock()
}

//鎖定當前的互斥量
//如果鎖已被使用,則調用goroutine
//阻塞直到互斥鎖可用。
func (m *Mutex) Lock() 

//對當前互斥量進行解鎖
//如果在進入解鎖時未鎖定m,則爲運行時錯誤。
//鎖定的互斥鎖與特定的goroutine無關。
//允許一個goroutine鎖定Mutex然後安排另一個goroutine來解鎖它。
func (m *Mutex) Unlock()

聲明一個互斥鎖:

var mutex sync.Mutex

不像C或Java的鎖類工具,我們可能會犯一個錯誤:忘記及時解開已被鎖住的鎖,從而導致流程異常。但Go由於存在defer,所以此類問題出現的概率極低。關於defer解鎖的方式如下:

var mutex sync.Mutex
func Write()  {
   mutex.Lock()
   defer mutex.Unlock()
}

如果對一個已經上鎖的對象再次上鎖,那麼就會導致該鎖定操作被阻塞,直到該互斥鎖回到被解鎖狀態

func main()  {
   var mutex sync.Mutex
   fmt.Println("start lock main")
   mutex.Lock()
   fmt.Println("get locked main")
   for i := 1;i<=3 ;i++  {
      go func(i int) {  
         fmt.Println("start lock ",i)
         mutex.Lock()
         fmt.Println("get locked ",i)
      }(i)
   }

   time.Sleep(time.Second)
   fmt.Println("Unlock the lock main")
   mutex.Unlock()
   fmt.Println("get unlocked main")
   time.Sleep(time.Second)
}

上面的示例中,我們在for循環之前開始加鎖,然後在每一次循環中創建一個協程,並對其加鎖,但是由於之前已經加鎖了,所以這個for循環中的加鎖會陷入阻塞直到main中的鎖被解鎖, time.Sleep(time.Second) 是爲了能讓系統有足夠的時間運行for循環,輸出結果如下:

start lock main
get locked main
start lock  3
start lock  1
start lock  2
Unlock the lock main
get unlocked main
get locked  3

最終在main解鎖後,三個協程會重新搶奪互斥鎖權,最終協程3獲勝。

互斥鎖鎖定操作的逆操作並不會導致協程阻塞,但是有可能導致引發一個無法恢復的運行時的panic,比如對一個未鎖定的互斥鎖進行解鎖時就會發生panic。避免這種情況的最有效方式就是使用defer。

我們知道如果遇到panic,可以使用recover方法進行恢復,但是如果對重複解鎖互斥鎖引發的panic卻是徒勞的(Go 1.8及以後)。

func main()  {
   defer func() {
      fmt.Println("Try to recover the panic")
      if p := recover();p!=nil{
         fmt.Println("recover the panic : ",p)
      }
   }()
   var mutex sync.Mutex
   fmt.Println("start lock")
   mutex.Lock()
   fmt.Println("get locked")
   fmt.Println("unlock lock")
   mutex.Unlock()
   fmt.Println("lock is unlocked")
   fmt.Println("unlock lock again")
   mutex.Unlock()
}

以上代碼試圖對重複解鎖引發的panic進行recover,但是我們發現操作失敗,輸出結果:

start lock
get locked
fatal error: sync: unlock of unlocked mutex
unlock lock
lock is unlocked
unlock lock again

goroutine 1 [running]:
runtime.throw(0x4c2b46, 0x1e)
    C:/Go/src/runtime/panic.go:619 +0x88 fp=0xc04207dea8 sp=0xc04207de88 pc=0x428978
sync.throw(0x4c2b46, 0x1e)
    C:/Go/src/runtime/panic.go:608 +0x3c fp=0xc04207dec8 sp=0xc04207dea8 pc=0x4288dc
sync.(*Mutex).Unlock(0xc042060080)
    C:/Go/src/sync/mutex.go:184 +0xc9 fp=0xc04207def0 sp=0xc04207dec8 pc=0x456b59
main.main()
    D:/GoDemo/src/MyGo/Demo_04.go:23 +0x1dd fp=0xc04207df88 sp=0xc04207def0 pc=0x48ca9d
runtime.main()
    C:/Go/src/runtime/proc.go:198 +0x20e fp=0xc04207dfe0 sp=0xc04207df88 pc=0x42a21e
runtime.goexit()
    C:/Go/src/runtime/asm_amd64.s:2361 +0x1 fp=0xc04207dfe8 sp=0xc04207dfe0 pc=0x44f791

雖然互斥鎖可以被多個協程共享,但還是建議將對同一個互斥鎖的加鎖解鎖操作放在同一個層次的代碼中。

2 讀寫鎖
讀寫鎖是針對讀寫操作的互斥鎖,可以分別針對讀操作與寫操作進行鎖定和解鎖操作 。
讀寫鎖的訪問控制規則如下:
①多個寫操作之間是互斥的
②寫操作與讀操作之間也是互斥的
③多個讀操作之間不是互斥的
在這樣的控制規則下,讀寫鎖可以大大降低性能損耗。

由標準庫代碼包中sync中的RWMutex結構體表示

// RWMutex是一個讀/寫互斥鎖,可以由任意數量的讀操作或單個寫操作持有。
// RWMutex的零值是未鎖定的互斥鎖。
//首次使用後,不得複製RWMutex。
//如果goroutine持有RWMutex進行讀取而另一個goroutine可能會調用Lock,那麼在釋放初始讀鎖之前,goroutine不應該期望能夠獲取讀鎖定。 
//特別是,這種禁止遞歸讀鎖定。 這是爲了確保鎖最終變得可用; 阻止的鎖定會阻止新讀操作獲取鎖定。
type RWMutex struct {
   w           Mutex  //如果有待處理的寫操作就持有
   writerSem   uint32 // 寫操作等待讀操作完成的信號量
   readerSem   uint32 //讀操作等待寫操作完成的信號量
   readerCount int32  // 待處理的讀操作數量
   readerWait  int32  // number of departing readers
}

sync中的RWMutex有以下幾種方法:

//對讀操作的鎖定
func (rw *RWMutex) RLock()
//對讀操作的解鎖
func (rw *RWMutex) RUnlock()
//對寫操作的鎖定
func (rw *RWMutex) Lock()
//對寫操作的解鎖
func (rw *RWMutex) Unlock()

//返回一個實現了sync.Locker接口類型的值,實際上是回調rw.RLock and rw.RUnlock.
func (rw *RWMutex) RLocker() Locker

Unlock會試圖喚醒所有因欲進行讀鎖定而被阻塞的協程,而 RUnlock 只會在已無任何讀鎖定的情況下,試圖喚醒一個因欲進行寫鎖定而被阻塞的協程。若對一個未被寫鎖定的讀寫鎖進行寫解鎖,就會引發一個不可恢復的panic,同理對一個未被讀鎖定的讀寫鎖進行讀寫鎖也會如此。

由於讀寫鎖控制下的多個讀操作之間不是互斥的,因此對於讀解鎖更容易被忽視。對於同一個讀寫鎖,添加多少個讀鎖定,就必要有等量的讀解鎖,這樣才能其他協程有機會進行操作。

func main() {
   var rwm sync.RWMutex
   for i := 0; i < 3; i++ {
      go func(i int) {
         fmt.Println("try to lock read ", i)
         rwm.RLock()
         fmt.Println("get locked ", i)
         time.Sleep(time.Second *2)
         fmt.Println("try to unlock for reading ", i)
         rwm.RUnlock()
         fmt.Println("unlocked for reading ", i)
      }(i)
   }
   time.Sleep(time.Millisecond * 1000)
   fmt.Println("try to lock for writing")
   rwm.Lock()
   fmt.Println("locked for writing")
}

上面的示例創建了三個協程用於對讀寫鎖的讀鎖定與讀解鎖操作。在 rwm.Lock()種會對main中協程進行寫鎖定,但是for循環中的讀解鎖尚未完成,因此會造成mian中的協程阻塞。當for循環中的讀解鎖操作都完成後就會試圖喚醒main中阻塞的協程,main中的寫鎖定纔會完成。輸出結果如下

try to lock read  0
get locked  0
try to lock read  2
get locked  2
try to lock read  1
get locked  1
try to lock for writing
try to unlock for reading  0
unlocked for reading  0
try to unlock for reading  2
unlocked for reading  2
try to unlock for reading  1
unlocked for reading  1
locked for writing

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章