2023 年 Go 併發庫的變化
2023 年來, Go 的併發庫又有了一些變化,這篇文章是對這些變化的綜述。小細節的變化,比如 typo、文檔變化等無關大局的變化就不介紹了。
sync.Once
Go 1.21.0 中增加了和 Once 相關的三個函數,便於 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)
這三個函數的功能分別是:
-
OnceFunc:返回一個函數
g
,多次調用這個函數g
,只會執行一次f
。如果f
執行時 panic, 則後續調用這個函數g
不會再執行f
, 但是每次調用都會 panic。 -
OnceValue:返回一個函數
g
,多次調用這個函數g
,只會執行一次f
,函數g
返回值類型是 T。比上一個g
多了一個返回值。panic 原理同上。 -
OnceValues:返回一個函數
g
,多次調用這個函數g
,只會執行一次f
,函數g
返回值類型是 (T1, T2)。比上一個g
又多了一個返回值。panic 原理同上。
當然理論上你還可以增加更多的函數,返回更多的返回值,因爲 Go 沒有 Tuple 類型,所以這裏還不能簡化函數g
的返回值爲 Tuple 類型。反正 Go 1.21.0 就只增加了這三個函數。
這個有什麼好處呢?先前我們使用sync.Once
的時候,比如初始化一個線程池,我們需要定義一個線程池的變量,每次訪問線程池變量的時候,我需要調用一下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
, 就可以簡化代碼:
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())
}
}
代碼略微簡化,獲取單例的時候只需調用返回的函數g
即可。
所以基本上,這三個函數只是對 sync.Once 做了封裝,更方便使用。
理解 copyChecker
我們知道, sync.Cond
有兩個字段noCopy
和checker
, noCopy
通過go vet
工具能夠靜態編譯時檢查出來,但是checker
是在運行時檢查的:
type Cond struct {
noCopy noCopy
// L is held while observing or changing the condition
L Locker
notify notifyList
checker copyChecker
}
先前copyChecker
的判斷條件如下,雖然簡單的三行,但是不容易理解:
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")
}
}
現在加上了註釋,解釋了這三行的意義:
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 步:
-
第一步是一個快速檢查, 直接比較
c
指針和c
本身的指針, 如果不相等則表示已被複制。這是最快的檢查路徑。 -
第二步確保
c
已經被初始化。使用 CAS (CompareAndSwap) 來初始化。如果 CAS 失敗, 說明c
已經在其他 goroutine 初始化, 或者被複制了。 -
第三步再次執行第一步的檢查。因爲這時我們清楚的知道
c
已經初始化了, 所以如果檢查失敗, 就可以確認c
被複制了。
整個邏輯就是使用 CAS 配合兩次指針檢查, 來確保判斷的正確性。
總的來說,第一步快速檢查是性能優化。第二步使用 CAS 確保初始化。第三步再次檢查來確保判斷。
sync.Map 的一處優化
先前, sync.Map
的 Range
函數的實現如下:
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)
, 會導致read
逃逸到堆上,通過下面的一個小技巧,避免了read
的逃逸(通過一個新的變量):
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] 對這個問題進行了分析。
sync.Once 的實現中 done 使用 atomic.Uint32 替換
先前sync.Once
的實現如下:
type Once struct {
done uint32
m Mutex
}
其中字段done
是一個uint32
類型,用來表示Once
是否已經執行過了。這個字段的類型是uint32
,而不是bool
,是因爲uint32
類型可以使用atomic
包的原子操作,而bool
類型不能。
現在sync.Once
的實現如下:
type Once struct {
done atomic.Uint32
m Mutex
}
自從 go 1.19 提供了對基本類型的原子封裝,Go 標準庫大量代碼都被atomic.XXX
類型鎖替換。
我個人認爲,目前這個修改相對於先前的實現,性能上在某些情況下可能會有性能的下降,我會專門寫一篇文章進行探討。
除了sync.Once
,還有一批類型使用了atomic.XXX
類型替換原來的使用方法,有必要可以進行替換麼?
sync.OnceFunc 初始實現的優化
初始的sync.OnceFunc
的實現如下:
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)
}
}
}
仔細看這段代碼,你會發現,傳遞給OnceFunc/OnceValue/OnceValues
的函數f
,即使執行完一次,只要返回的g
函數好活着沒有被垃圾回收,這個f
就一直存活。這是沒必要的,因爲f
只需要執行一次,執行完就可以被垃圾回收了。所以,這裏可以對f
進行一次優化,讓f
執行完就設置爲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 中, 新增加了一個WithCancelCause
方法 (func WithCancelCause(parent Context) (ctx Context, cancel CancelCauseFunc)
),我們在cancel
的時候可以把 cancel 的原因傳遞給WithCancelCause
產生的 Context,這樣可以通過context.Cause
方法獲取到cancel
的原因。
ctx, cancel := context.WithCancelCause(parent)
cancel(myError)
ctx.Err() // 返回 context.Canceled
context.Cause(ctx) // 返回 myError
當然這個實現只進行了一半,因爲超時相關的 Context 也需要增加這個功能,所以在 Go 1.21.0 中又新增了兩個相關的函數:
func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc)
func WithTimeoutCause(parent Context, timeout time.Duration, cause error) (Context, CancelFunc)
這兩個和WithCancelCause
還不太一樣,不是利用返回的 cancel 函數傳遞原因,而是直接在函數參數中傳遞原因。
Go 1.21.0 還增加了一個AfterFunc
函數,這個函數和time.AfterFunc
類似,但是返回的是一個Context
,這個Context
在超時後會自動取消,這個函數的實現如下:
func AfterFunc(ctx Context, f func()) (stop func() bool)
指定的Context
在在 done(超時或者取消),如果 context 已經 done, 那麼f
立即被調用。返回的stop
函數用來停止f
的調用,如果stop
被調用並且返回 true,f
不會被調用。
這是一個輔助函數,但是難以理解,估計這個函數不會被廣泛的使用。
其他一些小性能的優化比如type emptyCtx int
替換成type emptyCtx struct{}
等等就不用提了。
增加了一個func WithoutCancel(parent Context) Context
, 當 parent 被取消時,不會波及到這個函數返回的 Context。
Coroutines for Go
在今年 7 月,Russ Coxx 寫了一篇巨論:Coroutines for Go[2]。
個人不看好在 Go 標準庫實現這個東西,我感覺 Rob Pike 也不會同意,但是這個東西社區如果去實現一個庫,我覺得還是有可能的,返回如果大家不看好,社區的庫自然會消亡。
否則,漸漸的 Go 迷失了它的初心: 簡單好用。
社區的一些協程庫:
-
coroutine[3]
-
routine[4]
-
gocoro[5]
你在 go.dev 還能搜到一些,這裏就不贅述了。
golang.org/x/sync 沒有明顯改動
errgroup
支持使用withCancelCause
設置 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
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/sY5_IEDUg9A6Kl90xEkQfA