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
,例如 fmt
和 encoding/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.
**/
四、參考
- https://www.cnblogs.com/qcrao-2018/p/12736031.html 深度解密 Go 語言之 sync.Pool,作者:Stefno
- https://geektutu.com/post/hpg-sync-pool.html Go sync.Pool 複用對象,作者:極客兔兔
- https://pkg.go.dev/sync#Pool
- https://cs.opensource.google/go/go/+/refs/tags/go1.19:src/runtime/sema.go;l=482
- https://stackoverflow.com/questions/36857167/how-to-correctly-use-sync-cond