盤(pán)點(diǎn)總結(jié)2023年Go并發(fā)庫(kù)有哪些變化
引言
2023 年來(lái), Go 的并發(fā)庫(kù)又有了一些變化,這篇文章是對(duì)這些變化的綜述。小細(xì)節(jié)的變化,比如 typo、文檔變化等無(wú)關(guān)大局的變化就不介紹了。
sync.Once
Go 1.21.0 中增加了和 Once 相關(guān)的三個(gè)函數(shù),便于 Once 的使用。
func OnceFunc(f func()) func() func OnceValue[T any](f func( "T any") T) func() T func OnceValues[T1, T2 any](f func( "T1, T2 any") (T1, T2)) func() (T1, T2)
這三個(gè)函數(shù)的功能分別是:
OnceFunc:返回一個(gè)函數(shù)
g,多次調(diào)用這個(gè)函數(shù)g,只會(huì)執(zhí)行一次f。如果f執(zhí)行時(shí) panic, 則后續(xù)調(diào)用這個(gè)函數(shù)g不會(huì)再執(zhí)行f,但是每次調(diào)用都會(huì) panic。OnceValue:返回一個(gè)函數(shù)
g,多次調(diào)用這個(gè)函數(shù)g,只會(huì)執(zhí)行一次f,函數(shù)g返回值類型是 T。比上一個(gè)g多了一個(gè)返回值。panic 原理同上。OnceValues:返回一個(gè)函數(shù)
g,多次調(diào)用這個(gè)函數(shù)g,只會(huì)執(zhí)行一次f,函數(shù)g返回值類型是(T1, T2)。比上一個(gè)g又多了一個(gè)返回值。panic 原理同上。
當(dāng)然理論上你還可以增加更多的函數(shù),返回更多的返回值,因?yàn)?Go 沒(méi)有 Tuple 類型,所以這里還不能簡(jiǎn)化函數(shù)g的返回值為 Tuple 類型。反正 Go 1.21.0 就只增加了這三個(gè)函數(shù)。
這個(gè)有什么好處呢?先前我們使用sync.Once的時(shí)候,比如初始化一個(gè)線程池,我們需要定義一個(gè)線程池的變量,每次訪問(wèn)線程池變量的時(shí)候,我需要調(diào)用一下sync.Once.Do:
func TestOnce(t *testing.T) {
var pool any
var once sync.Once
var initFn = func() {
// init pool
pool = 1
}
for i := 0; i < 10; i++ {
once.Do(initFn)
t.Log(pool)
}
}
如果使用OnceValue,就可以簡(jiǎn)化代碼:
func TestOnceValue(t *testing.T) {
var initPool = func() any {
return 1
}
var poolGenerator = sync.OnceValue(initPool)
for i := 0; i < 10; i++ {
t.Log(poolGenerator())
}
}
代碼略微簡(jiǎn)化,獲取單例的時(shí)候只需調(diào)用返回的函數(shù)g即可。
所以基本上,這三個(gè)函數(shù)只是對(duì) sync.Once 做了封裝,更方便使用。
理解 copyChecker
我們知道, sync.Cond有兩個(gè)字段noCopy和checker, noCopy通過(guò)go vet工具能夠靜態(tài)編譯時(shí)檢查出來(lái),但是checker是在運(yùn)行時(shí)檢查的:
type Cond struct {
noCopy noCopy
// L is held while observing or changing the condition
L Locker
notify notifyList
checker copyChecker
}先前copyChecker的判斷條件如下,雖然簡(jiǎn)單的三行,但是不容易理解:
func (c *copyChecker) check() {
if uintptr(*c) != uintptr(unsafe.Pointer(c)) &&
!atomic.CompareAndSwapUintptr((*uintptr)(c), 0, uintptr(unsafe.Pointer(c))) &&
uintptr(*c) != uintptr(unsafe.Pointer(c)) {
panic("sync.Cond is copied")
}
}現(xiàn)在加上了注釋,解釋了這三行的意義:
func (c *copyChecker) check() {
// Check if c has been copied in three steps:
// 1. The first comparison is the fast-path. If c has been initialized and not copied, this will return immediately. Otherwise, c is either not initialized, or has been copied.
// 2. Ensure c is initialized. If the CAS succeeds, we're done. If it fails, c was either initialized concurrently and we simply lost the race, or c has been copied.
// 3. Do step 1 again. Now that c is definitely initialized, if this fails, c was copied.
if uintptr(*c) != uintptr(unsafe.Pointer(c)) &&
!atomic.CompareAndSwapUintptr((*uintptr)(c), 0, uintptr(unsafe.Pointer(c))) &&
uintptr(*c) != uintptr(unsafe.Pointer(c)) {
panic("sync.Cond is copied")
}
}主要邏輯
在以下 3 步:
第一步是一個(gè)快速檢查,直接比較
c指針和c本身的指針,如果不相等則表示已被復(fù)制。這是最快的檢查路徑。第二步確保
c已經(jīng)被初始化。使用 CAS (CompareAndSwap)來(lái)初始化。如果 CAS 失敗,說(shuō)明c已經(jīng)在其他 goroutine 初始化,或者被復(fù)制了。第三步再次執(zhí)行第一步的檢查。因?yàn)檫@時(shí)我們清楚的知道
c已經(jīng)初始化了,所以如果檢查失敗,就可以確認(rèn)c被復(fù)制了。
整個(gè)邏輯就是使用 CAS 配合兩次指針檢查,來(lái)確保判斷的正確性。
總的來(lái)說(shuō),第一步快速檢查是性能優(yōu)化。第二步使用 CAS 確保初始化。第三步再次檢查來(lái)確保判斷。
sync.Map 的一處優(yōu)化
先前, sync.Map 的 Range 函數(shù)的實(shí)現(xiàn)如下:
func (m *Map) Range(f func(key, value any) bool) {
...
if read.amended {
read = readOnly{m: m.dirty}
m.read.Store(&read)
m.dirty = nil
m.misses = 0
}
...
}其中有一段代碼:m.read.Store(&read),會(huì)導(dǎo)致read逃逸到堆上,通過(guò)下面的一個(gè)小技巧,避免了read的逃逸(通過(guò)一個(gè)新的變量):
func (m *Map) Range(f func(key, value any) bool) {
...
if read.amended {
read = readOnly{m: m.dirty}
copyRead := read
m.read.Store(©Read)
m.dirty = nil
m.misses = 0
}
...
}
issue #62404[1]對(duì)這個(gè)問(wèn)題進(jìn)行了分析。
sync.Once 的實(shí)現(xiàn)中 done 使用 atomic.Uint32 替換
先前sync.Once的實(shí)現(xiàn)如下:
type Once struct {
done uint32
m Mutex
}
其中字段done是一個(gè)uint32類型,用來(lái)表示Once是否已經(jīng)執(zhí)行過(guò)了。這個(gè)字段的類型是uint32,而不是bool,是因?yàn)?code>uint32類型可以使用atomic包的原子操作,而bool類型不能。
現(xiàn)在sync.Once的實(shí)現(xiàn)如下:
type Once struct {
done atomic.Uint32
m Mutex
}
自從 go 1.19 提供了對(duì)基本類型的原子封裝,Go 標(biāo)準(zhǔn)庫(kù)大量代碼都被atomic.XXX類型鎖替換。
我個(gè)人認(rèn)為,目前這個(gè)修改相對(duì)于先前的實(shí)現(xiàn),性能上在某些情況下可能會(huì)有性能的下降,我會(huì)專門(mén)寫(xiě)一篇文章進(jìn)行探討。
除了sync.Once,還有一批類型使用了atomic.XXX類型替換原來(lái)的使用方法,有必要可以進(jìn)行替換么?
sync.OnceFunc 初始實(shí)現(xiàn)的優(yōu)化
初始的sync.OnceFunc的實(shí)現(xiàn)如下:
func OnceFunc(f func()) func() {
var (
once Once
valid bool
p any
)
g := func() {
defer func() {
p = recover()
if !valid {
panic(p)
}
}()
f()
valid = true
}
return func() {
once.Do(g)
if !valid {
panic(p)
}
}
}
仔細(xì)看這段代碼,你會(huì)發(fā)現(xiàn),傳遞給OnceFunc/OnceValue/OnceValues的函數(shù)f,即使執(zhí)行完一次,只要返回的g函數(shù)好活著沒(méi)有被垃圾回收,這個(gè)f就一直存活。這是沒(méi)必要的,因?yàn)?code>f只需要執(zhí)行一次,執(zhí)行完就可以被垃圾回收了。所以,這里可以對(duì)f進(jìn)行一次優(yōu)化,讓f執(zhí)行完就設(shè)置為nil,這樣就可以被垃圾回收了。
func OnceFunc(f func()) func() {
var (
once Once
valid bool
p any
)
// Construct the inner closure just once to reduce costs on the fast path.
g := func() {
defer func() {
p = recover()
if !valid {
// Re-panic immediately so on the first call the user gets a
// complete stack trace into f.
panic(p)
}
}()
f()
f = nil // Do not keep f alive after invoking it.
valid = true // Set only if f does not panic.
}
return func() {
once.Do(g)
if !valid {
panic(p)
}
}
}
context
我們知道,在 Go 1.20 中, 新增加了一個(gè)WithCancelCause方法(func WithCancelCause(parent Context) (ctx Context, cancel CancelCauseFunc)),我們?cè)?code>cancel的時(shí)候可以把 cancel 的原因傳遞給WithCancelCause產(chǎn)生的 Context,這樣可以通過(guò)context.Cause方法獲取到cancel的原因。
ctx, cancel := context.WithCancelCause(parent) cancel(myError) ctx.Err() // 返回 context.Canceled context.Cause(ctx) // 返回 myError
當(dāng)然這個(gè)實(shí)現(xiàn)只進(jìn)行了一半,因?yàn)槌瑫r(shí)相關(guān)的 Context 也需要增加這個(gè)功能,所以在 Go 1.21.0 中又新增了兩個(gè)相關(guān)的函數(shù):
func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc) func WithTimeoutCause(parent Context, timeout time.Duration, cause error) (Context, CancelFunc)
這兩個(gè)和WithCancelCause還不太一樣,不是利用返回的 cancel 函數(shù)傳遞原因,而是直接在函數(shù)參數(shù)中傳遞原因。
Go 1.21.0 還增加了一個(gè)AfterFunc函數(shù),這個(gè)函數(shù)和time.AfterFunc類似,但是返回的是一個(gè)Context,這個(gè)Context在超時(shí)后會(huì)自動(dòng)取消,這個(gè)函數(shù)的實(shí)現(xiàn)如下:
func AfterFunc(ctx Context, f func()) (stop func() bool)
指定的Context在在 done(超時(shí)或者取消),如果 context 已經(jīng) done,那么f立即被調(diào)用。返回的stop函數(shù)用來(lái)停止f的調(diào)用,如果stop被調(diào)用并且返回 true,f不會(huì)被調(diào)用。
這是一個(gè)輔助函數(shù),但是難以理解,估計(jì)這個(gè)函數(shù)不會(huì)被廣泛的使用。
其他一些小性能的優(yōu)化比如type emptyCtx int替換成type emptyCtx struct{}等等就不用提了。
增加了一個(gè)func WithoutCancel(parent Context) Context, 當(dāng) parent 被取消時(shí),不會(huì)波及到這個(gè)函數(shù)返回的 Context。
Coroutines for Go
在今年 7 月,Russ Coxx 寫(xiě)了一篇巨論:Coroutines for Go[2]。
個(gè)人不看好在 Go 標(biāo)準(zhǔn)庫(kù)實(shí)現(xiàn)這個(gè)東西,我感覺(jué) Rob Pike 也不會(huì)同意,但是這個(gè)東西社區(qū)如果去實(shí)現(xiàn)一個(gè)庫(kù),我覺(jué)得還是有可能的,返回如果大家不看好,社區(qū)的庫(kù)自然會(huì)消亡。
否則,漸漸的 Go 迷失了它的初心: 簡(jiǎn)單好用。
社區(qū)的一些協(xié)程庫(kù):
coroutine[3]
routine[4]
gocoro[5]
你在 go.dev 還能搜到一些,這里就不贅述了。
golang.org/x/sync 沒(méi)有明顯改動(dòng)
errgroup支持使用withCancelCause設(shè)置 cause。singleflight的 panicError 增加 Unwrap 方法。
參考資料
[1]
issue #62404: https://github.com/golang/go/issues/62404
[2]
Coroutines for Go: https://research.swtch.com/coro
[3]
coroutine: https://github.com/stealthrocket/coroutine
[4]
routine: https://github.com/solarlune/routine
[5]
gocoro: https://github.com/SolarLune/gocoro
以上就是盤(pán)點(diǎn)總結(jié)2023年Go并發(fā)庫(kù)有哪些變化的詳細(xì)內(nèi)容,更多關(guān)于Go 并發(fā)庫(kù)變化的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- Go語(yǔ)言動(dòng)態(tài)并發(fā)控制sync.WaitGroup的靈活運(yùn)用示例詳解
- golang?waitgroup輔助并發(fā)控制使用場(chǎng)景和方法解析
- GO中sync包自由控制并發(fā)示例詳解
- Go語(yǔ)言并發(fā)處理效率響應(yīng)能力及在現(xiàn)代軟件開(kāi)發(fā)中的重要性
- Go中Goroutines輕量級(jí)并發(fā)的特性及效率探究
- Go中并發(fā)控制的實(shí)現(xiàn)方式總結(jié)
- Go語(yǔ)言單線程運(yùn)行也會(huì)有的并發(fā)問(wèn)題解析
- Go語(yǔ)言并發(fā)編程之控制并發(fā)數(shù)量實(shí)現(xiàn)實(shí)例
相關(guān)文章
Go語(yǔ)言Telnet回音服務(wù)器的實(shí)現(xiàn)
這篇文章主要介紹了Go語(yǔ)言Telnet回音服務(wù)器的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-01-01
go本地環(huán)境配置及vscode go插件安裝的詳細(xì)教程
這篇文章主要介紹了go本地環(huán)境配置及vscode go插件安裝的詳細(xì)教程,本文通過(guò)圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-05-05
Go語(yǔ)言實(shí)現(xiàn)生產(chǎn)者-消費(fèi)者模式的方法總結(jié)
這篇文章主要介紹了在?Go?語(yǔ)言中實(shí)現(xiàn)生產(chǎn)者消費(fèi)者模式的多種方法,并重點(diǎn)探討了通道、條件變量的適用場(chǎng)景和優(yōu)缺點(diǎn),需要的可參考一下2023-05-05
Go的固定時(shí)長(zhǎng)定時(shí)器和周期性時(shí)長(zhǎng)定時(shí)器
本文主要介紹了Go的固定時(shí)長(zhǎng)定時(shí)器和周期性時(shí)長(zhǎng)定時(shí)器,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-08-08
使用Go語(yǔ)言實(shí)現(xiàn)benchmark解析器
這篇文章主要為大家詳細(xì)介紹了如何使用Go語(yǔ)言實(shí)現(xiàn)benchmark解析器并實(shí)現(xiàn)及Web UI 數(shù)據(jù)可視化,文中的示例代碼講解詳細(xì),需要的小伙伴可以參考一下2025-04-04

