Go sync併發工具包

簡介

在Java中提供Sychronized關鍵字提供獨佔鎖,Lock類提供讀寫鎖。在sync包中實現的功能也是與鎖相關,包中主要包含的有:

  • sync.Map:併發安全 map
  • sync.Mutex:鎖
  • sync.RWMutex:讀寫鎖
  • sync.Once:只執行一次
  • sync.WaitGroup: goroutine 之間同步
  • sync.Pool:複用緩存池

sync.Map

key 和 value 類型都是 Any。意味着你要搞各 種類型斷言

image-20230203142912299

使用示例

package main

import (
	"fmt"
	"sync"
)

func main() {
	m := sync.Map{}
	m.Store("cat", "Tom")
	m.Store("mouse", "Jerry")

	// 這裏重新讀取出來的,就是
	val, ok := m.Load("cat")
	if ok {
		fmt.Println(len(val.(string)))
		fmt.Printf("%s \n", val)
	}
}

image-20230204181456737

sync.Mutex 和sync.RWMutex

使用案例

package main

import (
	"sync"
)

var mutex sync.Mutex
var rwMutex sync.RWMutex

func Mutex() {
	mutex.Lock()
	defer mutex.Unlock()
	// 你的代碼
}

func RwMutex() {
	// 加讀鎖
	rwMutex.RLock()
	defer rwMutex.RUnlock()

	// 也可以加寫鎖
	rwMutex.Lock()
	defer rwMutex.Unlock()
}

// 不可重入例子
func Failed1() {
	mutex.Lock()
	defer mutex.Unlock()

	// 這一句會死鎖
	// 但是如果你只有一個goroutine,那麼這一個會導致程序崩潰
	mutex.Lock()
	defer mutex.Unlock()
}

// 不可升級
func Failed2() {
	rwMutex.RLock()
	defer rwMutex.RUnlock()

	// 這一句會死鎖
	// 但是如果你只有一個goroutine,那麼這一個會導致程序崩潰
	mutex.Lock()
	defer mutex.Unlock()
}

mutex家族注意事項

  • 儘量用 RWMutext
  • 儘量用 defer 來釋放鎖,防止panic沒有釋放鎖
  • 不可重入:lock 之後,即便是同一個線程(goroutine),也無法再次加鎖(寫遞歸函數要小心)
  • 不可升級:加了讀鎖之後,如果試圖加寫鎖,鎖不升級

不可重入和不可升級,和很多語言的實現都是不同 的,因此要小心使用

sync.Once

package main

import (
	"fmt"
	"sync"
)

func main() {
	PrintOnce()
	PrintOnce()
	PrintOnce()
}

var once sync.Once

// 這個方法,不管調用幾次,只會輸出一次
func PrintOnce() {
	once.Do(func() {
		fmt.Println("只輸出一次")
	})
}

image-20230204182436334

sync.WaitGroup

WaitGroup,它有3個函數:

  • Add():在被等待的協程啓動前加1,代表要等待1個協程。
  • Done():被等待的協程執行Done,代表該協程已經完成任務,通知等待協程。
  • Wait(): 等待其他協程的協程,使用Wait進行等待。
type WaitGroup
func (wg *WaitGroup) Add(delta int){}
func (wg *WaitGroup) Done(){}
func (wg *WaitGroup) Wait(){}

下怎麼用WaitGroup實現上面的問題。

隊長先創建一個WaitGroup對象wg,

每個隊員都是1個協程, 隊長讓隊員出發前,使用wg.Add(),

隊員出發尋找鑰匙,隊長使用wg.Wait()等待(阻塞)所有隊員完成,

某個隊員完成時執行wg.Done(),等所有隊員找到鑰匙,

wg.Wait()則返回,完成了等待的過程,接下來就是開箱。

package main

import (
	"fmt"
	"sync"
)

func main() {
	// 鑰匙
	key := 0
	// 資本家隊長創建隊伍
	wg := sync.WaitGroup{}
	// 這個隊伍十個人
	wg.Add(10)
	for i := 1; i <= 10; i++ {
		go func(val int) {
			key = val
			// 打工人隊員找到了鑰匙
			wg.Done()
		}(i)
	}
	// 隊長等待隊伍集合,拿回鑰匙,如果不等待,鑰匙數量不定
	// 把這個註釋掉你會發現,鑰匙數量不知道會有多少個
	wg.Wait()
	fmt.Println(key)
}

sync.Pool

pool是什麼

Golang在 1.3 版本的時候,在sync包中加入一個新特性:Pool。 簡單的說:就是一個臨時對象池

爲什麼需要sync.pool

保存和複用臨時對象,減少內存分配,降低GC壓力
對象越多GC越慢,因爲Golang進行三色標記回收的時候,要標記的也越多,自然就慢了

sync.pool 特性

  1. 優先從自己的緩存裏面返回數據
  2. 如果不夠了,就創建一個新的,使用傳入的 New 回調
  3. 在 GC 的時候,sync.Pool 會被清空

sync.pool 缺陷

  1. 沒有超時機制
  2. 無法限制內存使用量
  3. GC 全部清掉

使用案例

sync.Pool 的一般使用步驟(以user爲例)

  1. 定義 user 結構體
  2. 爲 user 加上 Reset 方法。該方法用於重置對象, 接收器是指針
  3. 定義一個 sync.Pool
  4. 調用 Get 方法
  5. 使用defer 保證放回去 pool
  6. 重置 user
  7. 執行業務邏輯
package main

import (
	"fmt"
	"sync"
)

func main() {

	// 初始化一個pool
	pool := sync.Pool{
		// 默認的返回值設置,不寫這個參數,默認是nil
		New: func() interface{} {
			return &user{}
		}}

	// Get 返回的是 interface{},所以需要類型斷言
    // Get 方法會返回 Pool 已經存在的對象;如果沒有就使用New方法創建.
	u := pool.Get().(*user)
    // 對象或資源不用時,調用 Put 方法把對象或資源放回池子,
    // 池子裏面的對象啥時候真正釋放是由 go_runtime進行回收,是不受外部控制的
	// defer 還回去
	defer pool.Put(u)

	// 緊接着重置 u 這個對象
	u.Reset("Tom", "[email protected]")

	// 下邊就是使用 u 來完成你的業務邏輯
	fmt.Printf("%+v", u)
}

type user struct {
	Name  string
	Email string
}

// 一般來說,複用對象都要求我們取出來之後,
// 重置裏面的字段
func (u *user) Reset(name string, email string) {
	u.Email = email
	u.Name = name
}

image-20230204185807141

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