Go語言併發編程(3):sync包介紹和使用(上)-Mutex,RWMutex,WaitGroup,sync.Map

一、sync 包簡介

在併發編程中,爲了解決競爭條件問題,Go 語言提供了 sync 標準包,它提供了基本的同步原語,例如互斥鎖、讀寫鎖等。

sync 包使用建議:

除了 Once 和 WaitGroup 類型之外,大多數類型旨在供低級庫程序使用。更高級別的同步最好用 channel 通道和通信來完成。

sync 包中類型:

  • sync.Mutex 互斥鎖
  • sync.RWMutex 讀寫鎖
  • sync.WaitGroup 等待組(等待一組 goroutine 完成)
  • sync.Map 併發 Map
  • sync.Once 執行一次
  • sync.Pool 對象池
  • sync.Cond 條件變量

此外,sync 下還有一個包 atomic, 它提供了對數據的原子操作。

另外,Go 的擴展包也提供了信號量這種同步原語:

  • x/sync/semaphore

二、sync.Mutex 互斥鎖

sync.Mutex 是一個互斥鎖,它的作用就是保護臨界區,確保同一時間只有一個 Go 協程進入臨界區。

什麼是臨界區?爲什麼有臨界區?

在併發編程中,有一部分程序被併發訪問,這個訪問可能是多個協程/線程修改這部分程序數據,這樣的操作會導致意想不到的結果,爲了不讓操作導致意外結果,怎麼辦?就需要把這部分程序保護起來,一次只允許一個協程/線程訪問這部分區域。需要被保護的這部分程序區域就叫臨界區。

防止多個協程/線程同時進入臨界區,修改程序數據。

互斥鎖就是一種可以保護臨界區資源方式。

互斥鎖其實是一種最特殊的信號量,這個"量"只有 0 和 1,所以也叫互斥量。互斥量的值爲 0 和 1,用來表示加鎖和解鎖。互斥鎖是一種獨佔鎖,即同一時間只能有一個協程持有鎖,其他協程必須等待。

互斥鎖使得同一時刻只有一個協程執行某段程序,其他協程等待該協程執行完在搶鎖後執行。

image-20230302144218909

如上圖所示:g1 用互斥鎖保護臨界區,g2 在中間嘗試獲取鎖失敗,g1 離開臨界區釋放鎖,g2 獲取到鎖然後進行相應操作,操作完後釋放鎖離開臨界區。

第一次使用後不得複製 Mutex。

互斥鎖使用:

  • 互斥鎖有兩個方法 Lock() 加鎖和 Unlock() 解鎖,他們是成對出現。當一個協程對資源上鎖後,其他協程只能等待該協程解鎖之後,才能再次上鎖。
  • 它還有一個 TryLock(),go1.18 之後添加的。
    • 當一個 goroutine 調用此方法試圖獲取鎖時,如果這把鎖沒有被其他 goroutine 持有,那麼這個 goroutine 獲取鎖並返回 true;
    • 如果這把鎖已經被其它 goroutine 持有,或正準備給某個喚醒的 gorouine,那麼請求鎖的 goroutine 直接返回 false,不會阻塞在方法調用上。
Lock()
代碼段(臨界區)
Unlock()

爲了防止上鎖後忘記釋放鎖,實際使用中用 defer 來釋放鎖。

例子:

package main

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

func main() {
	var a = 0

	var lock sync.Mutex
	for i := 0; i < 100; i++ { // 併發 100 個goroutine
		go func(id int) {
			lock.Lock()
			defer lock.Unlock()
			a += 1
			fmt.Printf("goroutine %d, a=%d\n", id, a)
		}(i)
	}

	time.Sleep(time.Second) //等待1秒, 確保所有的協程執行完
}

三、sync.RWMutex 讀寫鎖

sync.RWMutex 讀寫鎖,對數據操進加鎖進一步細分,針對讀操作和寫操作分別進行加鎖和解鎖。

在讀寫鎖下,讀操作和讀操作之間不互斥,多個寫操作是互斥,讀操作和寫操作也是互斥。

  • 當一個 goroutine 獲取讀鎖之後,其它的 goroutine 此時想獲取讀鎖,那麼可以繼續獲取鎖,不用等待解鎖;此時想獲取寫鎖,就會阻塞等待直到讀解鎖;
  • 當一個 goroutine 獲取寫鎖之後,其它的 goroutine 無論是獲取讀鎖還是寫鎖,都會阻塞等待。

讀寫鎖的好處:

多個讀之間不互斥,讀鎖就可以降低對數據讀取加互斥鎖的性能損耗。而不像互斥鎖那樣對所有的數據操作,不管是讀還是寫,同等對待,都加一把大鎖處理。

在讀多寫少的場景下,更適合用讀寫鎖。

RWMutex 讀寫鎖的方法:

  • Mutex 的加鎖和解鎖:Lock() 和 Unlock()
  • 只讀加鎖和加鎖:RLock() 和 RUnlock()
    • RLock() 加讀鎖時如果存在寫鎖,則不能加鎖;當只有讀鎖或無鎖時,可以加讀鎖,且讀鎖可以加載多個。
    • RUnlock() 解讀鎖。沒有讀鎖情況下調用 RUlock() 會導致 panic。

釋放鎖用 defer 來釋放鎖

// 使用 RWMutex 的僞碼,當然正式代碼不會這樣寫,會用 defer 釋放鎖
mutex := sync.RWMutex{}

mutex.Lock()
// 操作的資源
mutex.Unlock()

mutex.RLock()
// 讀的資源
mutex.RUlock()

例子:

package main

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

var sum = 0
var rwMutex sync.RWMutex

func main() {
	// 併發寫
	for i := 1; i <= 50; i++ {
		go writeSum()
	}

	// 併發讀
	for i := 1; i <= 20; i++ {
		go fmt.Println("readSum: ", readSum())
	}

	time.Sleep(time.Second * 2) // 防止主程序退出,子協程還沒運行完
	fmt.Println("end sum: ", sum)
}

func writeSum() {
	rwMutex.Lock()         // 讀寫鎖
	defer rwMutex.Unlock() // 釋放鎖
	sum += 1
}

func readSum() int {
	rwMutex.RLock()         // 讀寫鎖加讀鎖
	defer rwMutex.RUnlock() // 釋放讀鎖
	return sum
}

四、sync.WaitGroup 等待組

sync.WaitGroup,等待一組或多個 goroutine 執行完成。

WaitGroup 內部有一個安全的計數器,它調用 Add(n int) 方法把計數器 +n;使用 Done() 方法,將計數器減 1,Done() 的底層是調用 Add(-1);調用 Wait() 方法等待所有的 goroutine 執行完,即計數器爲 0,Wait() 就返回。

WaitGroup 詳細原理,可以看我前面的文章:sync.WaitGroup源碼分析

  • WaitGroup 裏的方法:
  • Add(n),設置要等待的子 goroutine 數量,n 表示要等待數量
  • Done(),子 goroutine 執行完後,計數器減一
  • Wait(),阻塞等待所有子 goroutine 執行完

例子:

package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup

	wg.Add(2)

	go func() {
		defer wg.Done()
		fmt.Println("子 goroutine1")
	}()

	go func() {
		defer wg.Done()
		fmt.Println("子 goroutine2")
	}()

	wg.Wait() // 等待所有的子goroutine結束

	fmt.Println("程序運行結束")
}

五、sync.Map 併發Map

在 Go 語言中,內置數據結構 map 並不是併發安全的,所以官方就出了一個 sync.Map。

希望瞭解 sync.Map 的原理,可以看這篇文章:深入理解Go語言(05):sync.map原理分析

sync.Map 裏的常用方法:

go v1.20.1

  • Store(key, value any),設置鍵的值
  • Load(key any) (value any, ok bool),獲取值
  • Delete(key any),根據 key 刪除值
  • LoadAndDelete(key any) (value any, loaded bool),根據 key 刪除值,返回以前的值如果還存在
  • LoadOrStore(key, value any)(actual any, loaded bool),先根據 key 查找 value,如果找到則返回原來的值,loaded 爲 true;如果沒有找到 key 對應的 value 值,則存在 key,value 值並將存儲值返回,loaded 爲 false
  • Range(f func(key, value any) bool),遍歷 sync.Map 的元素

更多方法請查看:https://pkg.go.dev/sync#Map

例子:

package main

import (
	"fmt"
	"sync"
)

func main() {
	var syncmap sync.Map

	syncmap.Store("li", 12)
	syncmap.Store("han", "lu")
	syncmap.Store("mei", 34)

	fmt.Println(syncmap.Load("han"))

	// key 不存在
	val, ok := syncmap.LoadOrStore("lei", "lei")
	fmt.Println(val, ok)
	// key 存在
	val, ok = syncmap.LoadOrStore("han", "cunzai")
	fmt.Println(val, ok)

	syncmap.Delete("mei")

	syncmap.Range(func(k, v any) bool {
		fmt.Println("k-v: ", k, v)
		return true
	})
}

參考

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