首頁 > 軟體

GO語言並行之好用的sync包詳解

2022-12-29 14:00:55

sync.Map 並行安全的Map

反例如下,兩個Goroutine分別讀寫。

func unsafeMap(){
	var wg sync.WaitGroup
	m := make(map[int]int)
	wg.Add(2)
	go func() {
		defer wg.Done()
		for i := 0; i < 10000; i++ {
			m[i] = i
		}
	}()

	go func() {
		defer wg.Done()
		for i := 0; i < 10000; i++ {
			fmt.Println(m[i])
		}
	}()
	wg.Wait()
}

執行報錯:

0
fatal error: concurrent map read and map write

goroutine 7 [running]:
runtime.throw({0x10a76fa, 0x0})
......

使用並行安全的Map

func safeMap() {
	var wg sync.WaitGroup
	var m sync.Map
	wg.Add(2)
	go func() {
		defer wg.Done()
		for i := 0; i < 10000; i++ {
			m.Store(i, i)
		}
	}()

	go func() {
		defer wg.Done()
		for i := 0; i < 10000; i++ {
			fmt.Println(m.Load(i))
		}
	}()
	wg.Wait()
}
  • 不需要make就能使用
  • 還內建了StoreLoadLoadOrStoreDeleteRange等操作方法,自行體驗。

sync.Once 只執行一次

很多場景下我們需要確保某些操作在高並行的場景下只執行一次,例如只載入一次組態檔、只關閉一次通道等。

init 函數是當所在的 package 首次被載入時執行,若遲遲未被使用,則既浪費了記憶體,又延長了程式載入時間。

sync.Once 可以在程式碼的任意位置初始化和呼叫,因此可以延遲到使用時再執行,並行場景下是執行緒安全的。

在多數情況下,sync.Once 被用於控制變數的初始化,這個變數的讀寫滿足如下三個條件:

  • 當且僅當第一次存取某個變數時,進行初始化(寫);
  • 變數初始化過程中,所有讀都被阻塞,直到初始化完成;
  • 變數僅初始化一次,初始化完成後駐留在記憶體裡。
var loadOnce sync.Once
var x int
for i:=0;i<10;i++{
    loadOnce.Do(func() {
        x++
    })
}
fmt.Println(x)

輸出

1

sync.Cond 條件變數控制

sync.Cond 基於互斥鎖/讀寫鎖,它和互斥鎖的區別是什麼呢?

互斥鎖 sync.Mutex 通常用來保護臨界區和共用資源,條件變數 sync.Cond 用來協調想要存取共用資源的 goroutine

也就是在存在共用變數時,可以直接使用sync.Cond來協調共用變數,比如最常見的共用佇列,多消費多生產的模式。

我一開始也很疑惑為什麼不使用channelselect的模式來做生產者消費者模型(實際上也可以),這一節不是重點就不展開討論了。

建立範例

func NewCond(l Locker) *Cond

NewCond 建立 Cond 範例時,需要關聯一個鎖。

廣播喚醒所有

func (c *Cond) Broadcast()

Broadcast 喚醒所有等待條件變數 cgoroutine,無需鎖保護。

喚醒一個協程

func (c *Cond) Signal()

Signal 只喚醒任意 1 個等待條件變數 cgoroutine,無需鎖保護。

等待

func (c *Cond) Wait()

每個 Cond 範例都會關聯一個鎖 L(互斥鎖 *Mutex,或讀寫鎖 *RWMutex),當修改條件或者呼叫 Wait 方法時,必須加鎖。

舉個不恰當的例子,實現一個經典的生產者和消費者模式,但有先決條件:

  • 邊生產邊消費,可以多生產多消費。
  • 生產後通知消費。
  • 佇列為空時,暫停等待。
  • 支援關閉,關閉後等待消費結束。
  • 關閉後依然可以生產,但無法消費了。
var (
	cnt          int
	shuttingDown = false
	cond         = sync.NewCond(&sync.Mutex{})
)
  • cnt 為佇列,這裡直接用變數代替了,變數就是佇列長度。
  • shuttingDown 消費關閉狀態。
  • cond 現成的佇列控制。

生產者

func Add(entry int) {
	cond.L.Lock()
	defer cond.L.Unlock()
	cnt += entry
	fmt.Println("生產咯,來消費吧")
	cond.Signal()
}

消費者

func Get() (int, bool) {
	cond.L.Lock()
	defer cond.L.Unlock()
	for cnt == 0 && !shuttingDown {
		fmt.Println("未關閉但空了,等待生產")
		cond.Wait()
	}
	if cnt == 0 {
		fmt.Println("關閉咯,也消費完咯")
		return 0, true
	}
	cnt--
	return 1, false
}

關閉程式

func Shutdown() {
	cond.L.Lock()
	defer cond.L.Unlock()
	shuttingDown = true
	fmt.Println("要關閉咯,大家快消費")
	cond.Broadcast()
}

主程式

var wg sync.WaitGroup
	wg.Add(2)
	time.Sleep(time.Second)
	go func() {
		defer wg.Done()
		for i := 0; i < 10; i++ {
			go Add(1)
			if i%5 == 0 {
				time.Sleep(time.Second)
			}
		}
	}()
	go func() {
		defer wg.Done()
		shuttingDown := false
		for !shuttingDown {
			var cur int
			cur, shuttingDown = Get()
			fmt.Printf("當前消費 %d, 佇列剩餘 %d n", cur, cnt)
		}
	}()
	time.Sleep(time.Second * 5)
	Shutdown()
	wg.Wait()
  • 分別建立生產者與消費者。
  • 生產10個,每5個休息1秒。
  • 持續消費。
  • 主程式關閉佇列。

輸出

生產咯,來消費吧
當前消費 1, 佇列剩餘 0 
未關閉但空了,等待生產
生產咯,來消費吧
生產咯,來消費吧
當前消費 1, 佇列剩餘 1 
當前消費 1, 佇列剩餘 0 
未關閉但空了,等待生產
生產咯,來消費吧
生產咯,來消費吧
生產咯,來消費吧
當前消費 1, 佇列剩餘 2 
當前消費 1, 佇列剩餘 1 
當前消費 1, 佇列剩餘 0 
未關閉但空了,等待生產
生產咯,來消費吧
生產咯,來消費吧
生產咯,來消費吧
生產咯,來消費吧
當前消費 1, 佇列剩餘 1 
當前消費 1, 佇列剩餘 2 
當前消費 1, 佇列剩餘 1 
當前消費 1, 佇列剩餘 0 
未關閉但空了,等待生產
要關閉咯,大家快消費
關閉咯,也消費完咯
當前消費 0, 佇列剩餘 0

小結

1.sync.Map 並行安全的Map。

2.sync.Once 只執行一次,適用於設定讀取、通道關閉。

3.sync.Cond 控制協調共用資源。

以上就是GO語言並行之好用的sync包詳解的詳細內容,更多關於GO語言 sync包的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com