Go語言併發編程(4):sync包介紹和使用(下)-Once,Pool,Cond

sync包下:Once,Pool,Cond

一、sync.Once 執行一次

Once 簡介

  • sync.Once 是 Go 提供的讓函數只執行一次的一種實現。
  • 如果 once.Do(f) 被調用多次,只有第一次調用會調用 f。

常用場景:

  • 用於單例模式,比如初始化數據庫配置

Once 提供的方法:

  • 它只提供了一個方法 func (o *Once) Do(f func())

例 1,基本使用:

package main

import (
	"fmt"
	"sync"
)

func main() {
	var once sync.Once

	func1 := func() {
		fmt.Println("func1")
	}
	once.Do(func1)

	func2 := func() {
		fmt.Println("func2")
	}
	once.Do(func2)
}

運行輸出:

func1

多次調用 Once.Do() 只會執行第一次調用。

二、sync.Pool 複用對象

Pool 簡介

sync.Pool 可以單獨保存和複用臨時對象,可以認爲是一個存放對象的臨時容器或池子。也就是說 Pool 可以臨時管理多個對象。

存儲在 Pool 中的對象都可能隨時自動被 GC 刪除,也不會另行通知。所以它不適合像 socket 長連接或數據庫連接池。

一個 Pool 可以安全地同時被多個 goroutine 使用。

Pool 的目的是緩存已分配內存但未使用的對象供以後使用(重用),減輕 GC 的壓力,後面使用也不用再次分配內存。它可以構建高效、線程安全的空閒列表。

主要用途:

  • Pool 可以作爲一個臨時存儲池,把對象當作一個臨時對象存儲在池中,然後進行存或取操作,這樣對象就可以重用,不用進行內存分配,減輕 GC 壓力

Pool 的數據結構和 2 方法 Get() 和 Put():

// https://pkg.go.dev/[email protected]#Pool
type Pool struct {
    ...
    
	New func() any
}

func (p *Pool) Get() any

func (p *Pool) Put(x any)
  • Pool struct,裏面的 New 函數類型,聲明一個對象池
  • Get() 從對象池中獲取對象
  • Put() 對象使用完畢後,返回到對象池裏

例子1,基本使用

package main

import (
	"fmt"
	"sync"
)

func main() {
    // 創建一個 Pool
	pool := sync.Pool{
        // New 函數用處:當我們從 Pool 中用 Get() 獲取對象時,如果 Pool 爲空,則通過 New 先創建一個
        // 對象放入 Pool 中,相當於給一個 default 值
		New: func() interface{} {
			return 0
		},
	}

	pool.Put("lilei")
	pool.Put(1)

	fmt.Println(pool.Get())
	fmt.Println(pool.Get())
	fmt.Println(pool.Get())
	fmt.Println(pool.Get())
}

/** output:
lilei
1
0
0
**/

例子2,緩存臨時對象

from:https://geektutu.com/post/hpg-sync-pool.html 極客兔兔上的一個例子

package main

import "encoding/json"

type Student struct {
	Name   string
	Age    int32
	Remark [1024]byte
}

var buf, _ = json.Marshal(Student{Name: "Geektutu", Age: 25})

func unmarsh() {
	stu := &Student{}
	json.Unmarshal(buf, stu)
}

json 的反序列化的文本解析和網絡通信,當程序在高併發下,需要創建大量的臨時對象。這些對象又是分配在堆上,會給 GC 造成很大壓力,會嚴重影響性能。這時候 sync.Pool 就派上用場了。而且 Pool 大小是動態可伸縮的,高負載會動態擴容。

使用 sync.Pool

// 創建一個臨時對象池
var studentPool = sync.Pool{
	New: func() interface{} {
		return new(Student)
	},
}

// Get 和 Set 操作
func unmarshByPool() {
	stu := studentPool.Get().(*Student) // Get 獲取對象池中的對象,返回值是 interface{},需要類型轉換
	josn.Unmarshal(buf, stu)
	studentPool.Put(stu) // Put 對象使用完畢,返還給對象池
}

例子3,標準庫 fmt.Printf

Go 語言標準庫也大量使用了 sync.Pool,例如 fmtencoding/json

以下是 fmt.Printf 的源代碼(go/src/fmt/print.go):

// https://github.com/golang/go/blob/release-branch.go1.20/src/fmt/print.go#L120
type pp struct {
	buf buffer

	// arg holds the current item, as an interface{}.
	arg any

	// value is used instead of arg for reflect values.
	value reflect.Value

	// fmt is used to format basic items such as integers or strings.
	fmt fmt
	... ...
}

var ppFree = sync.Pool{
	New: func() any { return new(pp) },
}

func newPrinter() *pp {
	p := ppFree.Get().(*pp)
	p.panicking = false
	p.erroring = false
	p.wrapErrs = false
	p.fmt.init(&p.buf)
	return p
}

func (p *pp) free() {
	if cap(p.buf) > 64*1024 {
		p.buf = nil
	} else {
		p.buf = p.buf[:0]
	}
	if cap(p.wrappedErrs) > 8 {
		p.wrappedErrs = nil
	}

	p.arg = nil
	p.value = reflect.Value{}
	p.wrappedErrs = p.wrappedErrs[:0]
	ppFree.Put(p)
}

func (p *pp) Write(b []byte) (ret int, err error) {
	p.buf.write(b)
	return len(b), nil
}

func (p *pp) WriteString(s string) (ret int, err error) {
	p.buf.writeString(s)
	return len(s), nil
}

func Fprintf(w io.Writer, format string, a ...any) (n int, err error) {
	p := newPrinter()
	p.doPrintf(format, a)
	n, err = w.Write(p.buf)
	p.free()
	return
}

三、sync.Cond 條件變量

Cond 簡介

Cond 用互斥鎖和讀寫鎖實現了一種條件變量。

那什麼是條件?

比如在 Go 中,某個 goroutine 協程只有滿足了一些條件的情況下才能執行,否則等待。

比如併發中的協調共享資源情況,共享資源狀態發生了變化,在程序中可以看作是某種條件發生了變化,在鎖上等待的 goroutine,就可以通知它們,“你們要開始幹活了”。

那怎麼通知?

Go 中的 sync.Cond 在鎖的基礎上增加了一個消息通知的功能,保存了一個 goroutine 通知列表,用來喚醒一個或所有因等待條件變量而阻塞的 goroutine。它這個通知列表實際就是一個等待隊列,隊列裏存放了所有因等待條件變量(sync.Cond)而阻塞的 goroutine。

我們看下 sync.Cond 的數據結構:

// https://github.com/golang/go/blob/release-branch.go1.19/src/sync/cond.go#L36
type Cond struct {
	noCopy noCopy

	// L is held while observing or changing the condition
	L Locker

	notify  notifyList
	checker copyChecker
}
// https://cs.opensource.google/go/go/+/refs/tags/go1.19:src/sync/runtime2.go;drc=ad461f3261d755ab24222bc8bc30624e03646c3b;l=13
type notifyList struct {
	wait   uint32 // 下一個等待喚醒的 goroutine 索引,在鎖外自動增加
	notify uint32 // 下一個要通知的 goroutine 索引,只能在持有鎖的情況下寫入,讀取可以不要鎖
	lock   uintptr // key field of the mutex
	head   unsafe.Pointer // 鏈表頭
	tail   unsafe.Pointer // 鏈表尾
}

變量 notify 就是通知列表。

sync.Cond 用來協調那些訪問共享資源的 goroutine,當共享資源條件發生變化時,sync.Cond 就可以通知那些等待條件發生而阻塞的 goroutine。

既然是通知 goroutine 的功能,那與 channel 作爲通知功能有何區別?

與 channel 的區別

舉個例子,在併發編程裏,多個協程工作的程序,有一個協程 g1 正在接收數據,其它協程必須等待 g1 執行完,才能開始讀取到正確的數據。當 g1 接收完成後,怎麼通知其它所有協程?說:我讀完了,你們開始幹活了(開始讀取數據)。

想一想,用互斥鎖或channel?它們一般只能控制一個協程可以等待並讀取數據,並不能很方便的通知其它所有協程。

還有其它方法麼?想到的第一個方法,主動去問:

  • 給 g1 一個全局變量,用來標識是否接收完,其它協程反覆檢查該變量看是否接收完。

第二個方法,被動等通知,其它所有協程等通知:

  • 其它協程阻塞,g1 接收完畢後,通知其它協程。 這個阻塞可以是給每一個協程一個 channel 進行阻塞,g1 接收完,通知每一個 channel 解除阻塞。

(上面2種情況,讓我想到了網絡編程中的 select 和 epoll 的優化,select 不斷輪詢看數據是否接收完,epoll 把 socket 的讀和寫看作是事件,讀完了後主動回調函數進行處理。這個少了通知直接調用回調函數處理)

遇到這種情況,Go 給出了它的解決方法 - sync.Cond,就可以解決這個問題。它可以廣播喚醒所有等待的 goroutine。

sync.Cond 有一個喚醒列表,Broadcast 通過這個列表通知所有協程。

sync.Cond 使用情況總結

1、多個 goroutine 阻塞等待,一個 goroutine 通知所有,這時候用 sync.Cond。一個生產者,多個消費者

2、一個 goroutien 阻塞等待,一個 goroutine 通知一個,這時候用 鎖 或 channel

sync.Cond 的方法

從官網 https://pkg.go.dev/[email protected]#Cond 可以看出,有 4 個方法,分別是 NewCond(),Broadcast(),Signal(),Wait()。

  • NewCond:創建一個 sync.Cond 變量
  • Broadcast:廣播喚醒所有 wait 的 goroutine
  • Signal:一次只喚醒一個,哪個?最優先等待的 goroutine
  • Wait:等待條件喚醒
  • NewCond() 創建 Cond 實例
// https://github.com/golang/go/blob/release-branch.go1.19/src/sync/cond.go#L46
// NewCond returns a new Cond with Locker l.
func NewCond(l Locker) *Cond {
	return &Cond{L: l}
}

從上面方法可以看出,NewCond 創建實例需要傳入一個鎖,sync.NewCond(&sync.Mutex{}),返回一個帶有鎖的新 Cond。

  • BroadCast() 廣播喚醒所有
// https://github.com/golang/go/blob/release-branch.go1.19/src/sync/cond.go#L90
// Broadcast wakes all goroutines waiting on c.
//
// It is allowed but not required for the caller to hold c.L
// during the call.
func (c *Cond) Broadcast() {
	c.checker.check()
	runtime_notifyListNotifyAll(&c.notify)
}

廣播喚醒所有等待在條件變量 c 上的 goroutines。

  • Signal() 信號喚醒一個協程
// https://github.com/golang/go/blob/release-branch.go1.19/src/sync/cond.go#L81
// Signal wakes one goroutine waiting on c, if there is any.
//
// It is allowed but not required for the caller to hold c.L
// during the call.
//
// Signal() does not affect goroutine scheduling priority; if other goroutines
// are attempting to lock c.L, they may be awoken before a "waiting" goroutine.
func (c *Cond) Signal() {
	c.checker.check()
	runtime_notifyListNotifyOne(&c.notify)
}

信號喚醒等待在條件變量 c 上的一個 goroutine。

  • Wait() 等待
// https://github.com/golang/go/blob/release-branch.go1.19/src/sync/cond.go#L66
// Wait atomically unlocks c.L and suspends execution
// of the calling goroutine. After later resuming execution,
// Wait locks c.L before returning. Unlike in other systems,
// Wait cannot return unless awoken by Broadcast or Signal.
//
// Because c.L is not locked when Wait first resumes, the caller
// typically cannot assume that the condition is true when
// Wait returns. Instead, the caller should Wait in a loop:
//
//	c.L.Lock()
//	for !condition() {
//	    c.Wait()
//	}
//	... make use of condition ...
//	c.L.Unlock()
func (c *Cond) Wait() {
	c.checker.check()
	t := runtime_notifyListAdd(&c.notify)
	c.L.Unlock()
	runtime_notifyListWait(&c.notify, t)
	c.L.Lock()
}

Wait() 用於阻塞調用者,等待通知。向 notifyList 註冊一個通知,然後阻塞等待被通知。

看上面代碼:

runtime_notifyListAdd() 將當前 go 程添加到通知列表,等待通知

runtime_notifyListWait() 將當前 go 程休眠,接收到通知後才被喚醒

對條件的檢查,使用了 for !condition() 而非 if,是因爲當前協程被喚醒時,條件不一定符合要求,需要再次 Wait 等待下次被喚醒。爲了保險起見,使用 for 能夠確保條件符合要求後,再執行後續的代碼。

c.L.Lock()
for !condition() {
    c.Wait()
}
... make use of condition ...
c.L.Unlock()

例子1

來自:https://stackoverflow.com/questions/36857167/how-to-correctly-use-sync-cond 的一個例子

package main

import (
	"fmt"
	"sync"
)

// https://stackoverflow.com/questions/36857167/how-to-correctly-use-sync-cond
var sharedRsc = make(map[string]interface{})

func main() {
	var wg sync.WaitGroup
	wg.Add(2)

	fmt.Println("process start, sharedRsc len: ", len(sharedRsc))

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

	go func() {
		// this go routine wait for changes to the sharedRsc
		cond.L.Lock()
		for len(sharedRsc) == 0 { // 條件爲0,cond.Wait() 阻塞當前goroutine,並等待通知
			cond.Wait()
		}

		fmt.Println("sharedRsc[res1]:", sharedRsc["res1"])
		cond.L.Unlock()
		wg.Done()
	}()

	go func() {
		// this go routine wait for changes to the sharedRsc
		cond.L.Lock()
		for len(sharedRsc) == 0 { // 條件爲0,cond.Wait() 阻塞當前goroutine,並等待通知
			cond.Wait()
		}
		fmt.Println("sharedRsc[res2]:", sharedRsc["res2"])
		cond.L.Unlock()
		wg.Done()
	}()

	// this one writes changes to sharedRsc
	cond.L.Lock()
	sharedRsc["res1"] = "one"
	sharedRsc["res2"] = "two"
	cond.Broadcast() // 通知所有獲取鎖的 goroutine
	cond.L.Unlock()

	wg.Wait()

	fmt.Print("process end!!!")
}
/**
作者:garbagecollector
Having said that, using channels is still the recommended way to pass data around if the situation permitting.
作者建議:如果條件允許,channel 還是最好的數據通信方式
Note: sync.WaitGroup here is only used to wait for the goroutines to complete their executions.
**/

四、參考

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