golang 多協程的同步方法總結

菸草的香味.發表於2020-09-30

之前用 go 寫一個小工具的時候, 用到了多個協程之間的通訊, 當時隨手查了查, 結果查出來一大坨, 簡單記錄一下. golang中多個協程之間是如何進行通訊及資料同步的嘞.

共享變數

一個最簡單, 最容易想到的, 就是通過全域性變數的方式, 多個協程讀寫同一個變數. 但對同一個變數的更改, 就不得不加鎖了, 否則極易引發資料問題. 一般系統庫都提供基本的鎖, go 也提供了.

package main

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

var num = 0
// 互斥鎖
var mutex = sync.Mutex{}
// 讀寫鎖
var rwMutex = sync.RWMutex{}

func main() {
	for i := 0; i < 100; i++ {
		go incrNum()
	}
	time.Sleep(2)
	fmt.Println(num)
}

func incrNum() {
	mutex.Lock()
	num = num + 1
	mutex.Unlock()
}

僅執行一次

當查詢鎖查到sync這個模組時, 發現它下面的物件並沒有幾個, 都是針對協程同步的各個方面給出的解決方案. 所以我就一個一個看文件試了試.

當你需要對環境, 連線池等等資源進行初始化時, 這種操作只需要執行一次, 這時候就需要它了. sync.Once物件可以保證僅執行一次. 和 init 方法有些類似, 不過 init 方法是在模組首次載入時執行, 而sync.Once是在首次呼叫時執行. (其實現就是一個計數器加一個互斥鎖)

package main

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

var num = 0
var once = sync.Once{}

func main() {
	for i := 0; i < 100; i++ {
		go once.Do(incrNum)
	}
	time.Sleep(2)
	fmt.Println(num)
}

func incrNum() {
	num = num + 1
}

等待其他協程處理

某個協程需要等第一階段的所有協程處理完畢, 才能開始執行第二階段. 這個時候, 等待其他協程就可以通過sync.WaitGroup 來實現. (當然, 也可以通過一個共享計數器變數來實現).

package main

import (
	"fmt"
	"sync"
)

var waitGroup = sync.WaitGroup{}

func main() {
	for i := 0; i < 100; i++ {
		go incrNum()
	}
	// 等待其他協程處理完畢(共享變數為0)
	waitGroup.Wait()
	fmt.Println("don")
}

func incrNum() {
	// 增加需要等待的協程數量(共享變數+1)
	waitGroup.Add(1)
	// do something
	// 標記當前協程處理完成(共享變數-1)
	waitGroup.Done()
}

訊息通知

多個協程啟動時, 等待某個命令到來時執行命令, 喚醒等待協程. go 對此類操作也進行了處理, 感覺好貼心哦. 但是經過測試, 即使沒有空閒的協程, 喚醒命令同樣能夠發出去, 所以需要注意一下.

package main

import (
	"sync"
)

var mutex = &sync.Mutex{}
var cond = sync.NewCond(mutex)

func main() {
	for i := 0; i < 100; i++ {
		go incrNum()
	}
	// 傳送命令給一個隨機獲得鎖的協程
	cond.Signal()
	// 傳送命令給所有獲得鎖的協程
	cond.Broadcast()
}

func incrNum() {
	// 獲取鎖, 標識當前協程可以處理命令
	cond.L.Lock()
	// 可新增退出執行命令佇列的條件
	for true {
		// 等待命令
		cond.Wait()
		// do something
	}
	// 釋放鎖, 標記命令處理完畢, 退出協程
	cond.L.Unlock()
}

多協程 map

普通的 map 在多協程操作時, 是不支援併發寫入的. go貼心的給封裝了支援併發寫入的map. 同時也提供了針對map的基本操作.

package main

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

var m = sync.Map{}

func main() {
	for i := 0; i < 100; i++ {
		go func() {
			m.Store("1", 1)
		}()
	}
	time.Sleep(time.Second * 2)
	// 遍歷 map
	m.Range(func(key, value interface{}) bool {
		// 返回 false 結束遍歷
		return true
	})
	// 讀取變數, 若不存在則設定
	m.LoadOrStore("1", 3)
	// 刪除 key
	m.Delete("1")
	// 讀取變數
	load, _ := m.Load("1")
	fmt.Println(load)
}

多協程物件池

對於資料庫連線池應該並不陌生. 而sync.Pool物件是go封裝的協程安全的物件池. 物件池的使用十分簡單, 存/取

package main

import (
	"sync"
)

var p = sync.Pool{
	// 當池子中沒有物件了, 用於建立新物件
	New: func() interface {}{
		return "3"
	},
}

func main() {
	// 從池子中獲取一個物件
	r := p.Get()
	// 用完後將物件放回池子中
	p.Put(r)
}

sync 簡單總結

針對go系統的sync模組, 提供的基礎功能如下:

  1. 互斥鎖 Mutex
  2. 讀寫鎖 RWMutex
  3. 函式單次執行 Once
  4. 協程執行等待 WaitGroup
  5. 協程訊息通知 Cond
  6. 多協程 map Map
  7. 多協程物件池 Pool

幾個都簡單試過之後, 發現sync模組針對常用的幾個多協程工具進行了封裝, 想來可以基本滿足日常使用了.

終極通訊-channel

channel是一個協程安全的通訊管道, 簡單理解為資料從一側放入, 從另一側拿出. 這玩意感覺能玩出花來, 還不太理解, 留到國慶研究.

相關文章