28. 學習 Go 協程:互斥鎖和讀寫鎖

Hi,你們好,我是明哥。git

在本身學習 Golang 的這段時間裏,我寫了詳細的學習筆記放在個人我的微信公衆號 《Go編程時光》,對於 Go 語言,我也算是個初學者,所以寫的東西應該會比較適合剛接觸的同窗,若是你也是剛學習 Go 語言,不防關注一下,一塊兒學習,一塊兒成長。github

個人在線博客:golang.iswbm.com 個人 Github:github.com/iswbm/GolangCodingTimegolang


在 「19. 學習 Go 協程:詳解信道/通道」這一節裏我詳細地介紹信道的一些用法,要知道的是在 Go 語言中,信道的地位很是高,它是 first class 級別的,面對併發問題,咱們始終應該優先考慮使用信道,若是經過信道解決不了的,不得不使用共享內存來實現併發編程的,那 Golang 中的鎖機制,就是你繞不過的知識點了。編程

今天就來說一講 Golang 中的鎖機制。安全

在 Golang 裏有專門的方法來實現鎖,仍是上一節裏介紹的 sync 包。bash

這個包有兩個很重要的鎖類型微信

一個叫 Mutex, 利用它能夠實現互斥鎖。併發

一個叫 RWMutex,利用它能夠實現讀寫鎖。函數

1. 互斥鎖 :Mutex

使用互斥鎖(Mutex,全稱 mutual exclusion)是爲了來保護一個資源不會由於併發操做而引發衝突致使數據不許確。性能

舉個例子,就像下面這段代碼,我開啓了三個協程,每一個協程分別往 count 這個變量加1000次 1,理論上看,最終的 count 值應試爲 3000

package main

import (
	"fmt"
	"sync"
)

func add(count *int, wg *sync.WaitGroup) {
	for i := 0; i < 1000; 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)
}
複製代碼

可運行屢次的結果,都不相同

// 第一次
count 的值爲: 2854

// 第二次
count 的值爲: 2673

// 第三次
count 的值爲: 2840
複製代碼

緣由就在於這三個協程在執行時,先讀取 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 < 1000; 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 的值爲: 3000
複製代碼

使用 Mutext 鎖雖然很簡單,但仍然有幾點須要注意:

  • 同一協程裏,不要在還沒有解鎖時再次使加鎖
  • 同一協程裏,不要對已解鎖的鎖再次解鎖
  • 加了鎖後,別忘了解鎖,必要時使用 defer 語句

3. 讀寫鎖:RWMutex

Mutex 是最簡單的一種鎖類型,他提供了一個傻瓜式的操做,加鎖解鎖加鎖解鎖,讓你不須要再考慮其餘的。

簡單同時意味着在某些特殊狀況下有可能會形成時間上的浪費,致使程序性能低下。

舉個例子,咱們平時去圖書館,要嘛是去借書,要嘛去還書,借書的流程繁鎖,沒有辦卡的還要讓管理員給你辦卡,所以借書一般都要排老長的隊,假設圖書館裏只有一個管理員,按照 Mutex(互斥鎖)的思想, 這個管理員同一時刻只能服務一我的,這就意味着,還書的也要跟借書的一塊兒排隊。

可還書的步驟很是簡單,可能就把書給管理員掃下碼就能夠走了。

若是讓還書的人,跟借書的人一塊兒排隊,那估計有不少人都不樂意了。

所以,圖書館爲了提升整個流程的效率,就容許還書的人,不須要排隊,能夠直接自助還書。

圖書管將館裏的人分得更細了,對於讀者的不一樣需求提供了不一樣的方案。提升了效率。

RWMutex,也是如此,它將程序對資源的訪問分爲讀操做和寫操做

  • 爲了保證數據的安全,它規定了當有人還在讀取數據(即讀鎖佔用)時,不允計有人更新這個數據(即寫鎖會阻塞)
  • 爲了保證程序的效率,多我的(線程)讀取數據(擁有讀鎖)時,互不影響不會形成阻塞,它不會像 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()
}
複製代碼

輸出以下

第 1 個協程準備開始... 
第 0 個協程準備開始... 
第 3 個協程準備開始... 
第 2 個協程準備開始... 
準備釋放寫鎖,讀鎖再也不阻塞
第 2 個協程得到讀鎖, sleep 1s 後,釋放鎖
第 3 個協程得到讀鎖, sleep 1s 後,釋放鎖
第 1 個協程得到讀鎖, sleep 1s 後,釋放鎖
第 0 個協程得到讀鎖, sleep 1s 後,釋放鎖
程序退出...
複製代碼

相關文章
相關標籤/搜索