網(wǎng)站首頁(yè) 編程語(yǔ)言 正文
前言
在此前一篇文章中我們了解了?Golang Mutex 原理解析,今天來(lái)看一個(gè)官方給出的 Mutex 應(yīng)用場(chǎng)景:sync.Once。
1. 定位
Once is an object that will perform exactly one action.
sync.Once
?是 Go 標(biāo)準(zhǔn)庫(kù)提供的使函數(shù)只執(zhí)行一次的實(shí)現(xiàn),常應(yīng)用于單例模式,例如初始化配置、保持?jǐn)?shù)據(jù)庫(kù)連接等。它可以在代碼的任意位置初始化和調(diào)用,因此可以延遲到使用時(shí)再執(zhí)行,并發(fā)場(chǎng)景下是線程安全的。
2. 對(duì)外接口
Once 對(duì)外僅暴露了唯一的方法?Do(f func())
,f 為需要執(zhí)行的函數(shù)。
// Do calls the function f if and only if Do is being called for the // first time for this instance of Once. In other words, given // var once Once // if once.Do(f) is called multiple times, only the first call will invoke f, // even if f has a different value in each invocation. A new instance of // Once is required for each function to execute. // // Do is intended for initialization that must be run exactly once. Since f // is niladic, it may be necessary to use a function literal to capture the // arguments to a function to be invoked by Do: // config.once.Do(func() { config.init(filename) }) // // Because no call to Do returns until the one call to f returns, if f causes // Do to be called, it will deadlock. // // If f panics, Do considers it to have returned; future calls of Do return // without calling f. // func (o *Once) Do(f func())
結(jié)合注釋,我們來(lái)看看?Do?方法有哪些需要注意的:
- 只有在當(dāng)前的 Once 實(shí)例第一次調(diào)用?
Do
?方法時(shí),才會(huì)真正執(zhí)行?f
。哪怕在多次調(diào)用?Do
?中間?f
?的值有所變化,也只會(huì)被實(shí)際調(diào)用一次; -
Do
?針對(duì)的是只希望執(zhí)行一次的初始化操作,由于f
?是沒(méi)有參數(shù)的,如果需要傳參,可以采用包裝一層 func 的形式來(lái)實(shí)現(xiàn):config.once.Do(func() { config.init(filename) })
- 在對(duì)
f
?的調(diào)用返回之前,不會(huì)返回對(duì)Do
的調(diào)用,所以如果f方法中又調(diào)用來(lái)Do方法,將會(huì)死鎖。所以不要做這樣的操作:
func main() { var once sync.Once once.Do(func() { once.Do(func() { fmt.Println("hello kenmawr.") }) }) }
- 如果?
f
?拋出了 panic,此時(shí)Do
會(huì)認(rèn)為f
已經(jīng)返回,后續(xù)多次調(diào)用Do
也不會(huì)再觸發(fā)對(duì)?f
?的調(diào)用。
3. 實(shí)戰(zhàn)用法
sync.Once 的場(chǎng)景很多,但萬(wàn)變不離其宗的落腳點(diǎn)在于:任何只希望執(zhí)行一次的操作。
基于此,我們可以發(fā)現(xiàn)很多具體的應(yīng)用場(chǎng)景落地,比如某個(gè)資源的清理,全局變量的初始化,單例模式等,它們本質(zhì)都是一樣的。這里簡(jiǎn)單列幾個(gè),大家可以直接參考代碼熟悉。
3.1 初始化
很多同學(xué)可能會(huì)有疑問(wèn),我直接在?init()
?函數(shù)里面做初始化不就可以了嗎?效果上是一樣的,為什么還要用 sync.Once,這樣還需要多聲明一個(gè) once 對(duì)象。
原因在于:init()
?函數(shù)是在所在包首次被加載時(shí)執(zhí)行,若未實(shí)際使用,既浪費(fèi)了內(nèi)存,又延緩了程序啟動(dòng)時(shí)間。而?sync.Once
?可以在任何位置調(diào)用,而且是并發(fā)安全的,我們可以在實(shí)際依賴某個(gè)變量時(shí)才去初始化,這樣「延遲初始化」從功能上講并無(wú)差異,但可以有效地減少不必要的性能浪費(fèi)。
我們來(lái)看 Golang 官方的 html 庫(kù)中的一個(gè)例子,我們經(jīng)常使用的轉(zhuǎn)義字符串函數(shù)
func UnescapeString(s string) string
在進(jìn)入函數(shù)的時(shí)候,首先就會(huì)依賴包里內(nèi)置的?populateMapsOnce
?實(shí)例(本質(zhì)是一個(gè) sync.Once) 來(lái)執(zhí)行初始化?entity
?的操作。這里的entity
是一個(gè)包含上千鍵值對(duì)的?map
,如果init()
時(shí)就去初始化,會(huì)浪費(fèi)內(nèi)存。
var populateMapsOnce sync.Once var entity map[string]rune func populateMaps() { entity = map[string]rune{ "AElig;": '\U000000C6', "AMP;": '\U00000026', "Aacute;": '\U000000C1', "Abreve;": '\U00000102', "Acirc;": '\U000000C2', // 省略后續(xù)鍵值對(duì) } } func UnescapeString(s string) string { populateMapsOnce.Do(populateMaps) i := strings.IndexByte(s, '&') if i < 0 { return s } // 省略后續(xù)的實(shí)現(xiàn) ... }
3.2 單例模式
開發(fā)中我們經(jīng)常會(huì)實(shí)現(xiàn)?Getter
?來(lái)暴露某個(gè)非導(dǎo)出的變量,這個(gè)時(shí)候就可以把?once.Do
?放到?Getter
?里面,完成單例的創(chuàng)建。
package main import ( "fmt" "sync" ) type Singleton struct{} var singleton *Singleton var once sync.Once func GetSingletonObj() *Singleton { once.Do(func() { fmt.Println("Create Obj") singleton = new(Singleton) }) return singleton } func main() { var wg sync.WaitGroup for i := 0; i < 5; i++ { wg.Add(1) go func() { defer wg.Done() obj := GetSingletonObj() fmt.Printf("%p\n", obj) }() } wg.Wait() } /*--------- 輸出 ----------- Create Obj 0x119f428 0x119f428 0x119f428 0x119f428 0x119f428 **/
3.3 關(guān)閉channel
一個(gè)channel如果已經(jīng)被關(guān)閉,再去關(guān)閉的話會(huì) panic,此時(shí)就可以應(yīng)用 sync.Once 來(lái)幫忙。
type T int type MyChannel struct { c chan T once sync.Once } func (m *MyChannel) SafeClose() { // 保證只關(guān)閉一次channel m.once.Do(func() { close(m.c) }) }
4. 原理
在 sync 的源碼包中,Once 的定義是一個(gè) struct,所有定義和實(shí)現(xiàn)去掉注釋后不過(guò) 30行,我們直接上源碼來(lái)分析:
package sync import ( "sync/atomic" ) // 一個(gè) Once 實(shí)例在使用之后不能被拷貝繼續(xù)使用 type Once struct { done uint32 // done 表明了動(dòng)作是否已經(jīng)執(zhí)行 m Mutex } func (o *Once) Do(f func()) { if atomic.LoadUint32(&o.done) == 0 { o.doSlow(f) } } func (o *Once) doSlow(f func()) { o.m.Lock() defer o.m.Unlock() if o.done == 0 { defer atomic.StoreUint32(&o.done, 1) f() } }
這里有兩個(gè)非常巧妙的設(shè)計(jì)值得學(xué)習(xí),我們參照注釋來(lái)看一下:
- 結(jié)構(gòu)體字段順序?qū)λ俣鹊挠绊?我們來(lái)看一下帶注釋的 Once 結(jié)構(gòu)定義
type Once struct { // done indicates whether the action has been performed. // It is first in the struct because it is used in the hot path. // The hot path is inlined at every call site. // Placing done first allows more compact instructions on some architectures (amd64/386), // and fewer instructions (to calculate offset) on other architectures. done uint32 m Mutex }
sync.Once絕大多數(shù)場(chǎng)景都會(huì)訪問(wèn)o.done
,訪問(wèn)?done
?的機(jī)器指令是處于hot path上,hot path表示程序非常頻繁執(zhí)行的一系列指令。由于結(jié)構(gòu)體第一個(gè)字段的地址和結(jié)構(gòu)體的指針是相同的,如果是第一個(gè)字段,直接對(duì)結(jié)構(gòu)體的指針解引用即可,如果是其他的字段,除了結(jié)構(gòu)體指針外,還需要計(jì)算與第一個(gè)值的偏移,所以將done放在第一個(gè)字段,則CPU減少了一次偏移量的計(jì)算,訪問(wèn)速度更快。
- 為何不使用 CAS 來(lái)達(dá)到執(zhí)行一次的效果
其實(shí)使用?atomic.CompareAndSwapUint32
?是一個(gè)非常直觀的方案,這樣的話?Do
?的實(shí)現(xiàn)就變成了
func (o *OnceA) Do(f func()) { if !atomic.CompareAndSwapUint32(&o.done, 0, 1) { return } f() }
這樣的問(wèn)題在于,一旦出現(xiàn) CAS 失敗的情況,成功協(xié)程會(huì)繼續(xù)執(zhí)行?f
,但其他失敗協(xié)程不會(huì)等待?f
?執(zhí)行結(jié)束。而Do
?的API定位對(duì)此有著強(qiáng)要求,當(dāng)一次?once.Do
?返回時(shí),執(zhí)行的?f
?一定是完成的狀態(tài)。
對(duì)此,sync.Once 官方給出的解法是:
Slow path falls back to a mutex, and the atomic.StoreUint32 must be delayed until after f returns.
我們?cè)賮?lái)結(jié)合?doSlow()
?看一看這里是怎么解決這個(gè)并發(fā)問(wèn)題的:
func (o *Once) Do(f func()) { if atomic.LoadUint32(&o.done) == 0 { o.doSlow(f) } } func (o *Once) doSlow(f func()) { o.m.Lock() defer o.m.Unlock() if o.done == 0 { defer atomic.StoreUint32(&o.done, 1) f() } }
-
atomic.LoadUint32
?用于原子加載地址(也就是 &o.done),返回加載到的值; - o.done 為 0 是代表尚未執(zhí)行。若同時(shí)有兩個(gè) goroutine 進(jìn)來(lái),發(fā)現(xiàn) o.done 為 0(此時(shí)?
f
?尚未執(zhí)行),就會(huì)進(jìn)入?o.doSlow(f)
?的慢路徑中(slow path); -
doSlow
?使用?sync.Mutex
?來(lái)加鎖,一個(gè)協(xié)程進(jìn)去,其他的被阻塞在鎖的地方(注意,此時(shí)是阻塞,不是直接返回,這是和 CAS 方案最大的差別); - 經(jīng)過(guò)?
o.m.Lock()
?獲取到鎖以后,如果此時(shí) o.done 還是 0,意味著依然沒(méi)有被執(zhí)行,此時(shí)就可以放心的調(diào)用?f
來(lái)執(zhí)行了。否則,說(shuō)明當(dāng)前協(xié)程在被阻塞的過(guò)程中,已經(jīng)失去了調(diào)用f
?的機(jī)會(huì),直接返回。 -
defer atomic.StoreUint32(&o.done, 1)
?是這里的精華,必須等到f()
?返回,在 defer 里才能夠去更新 o.done 的值為 1。
5. 避坑
- 不要拷貝一個(gè) sync.Once 使用或作為參數(shù)傳遞,然后去執(zhí)行?
Do
,值傳遞時(shí)?done
?會(huì)歸0,無(wú)法起到限制一次的效果。 - 不要在?
Do
?的?f
?中嵌套調(diào)用?Do
。
原文鏈接:https://juejin.cn/post/7088305487753510925
相關(guān)推薦
- 2022-10-01 C#?將?Stream?保存到文件的方法_C#教程
- 2022-04-11 Python如何在終端彩色打印輸出_python
- 2023-02-04 Qt信號(hào)與槽知識(shí)點(diǎn)總結(jié)歸納_C 語(yǔ)言
- 2022-12-07 Rust?for循環(huán)語(yǔ)法糖背后的API場(chǎng)景分析_Rust語(yǔ)言
- 2023-05-06 docker?search命令的具體使用_docker
- 2022-11-23 Android開發(fā)Jetpack?Compose元素Modifier特性詳解_Android
- 2022-07-21 微信小程序使用vant weapp報(bào)錯(cuò)
- 2022-09-08 Prometheus和NodeExporter安裝監(jiān)控?cái)?shù)據(jù)說(shuō)明_其它綜合
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細(xì)win安裝深度學(xué)習(xí)環(huán)境2025年最新版(
- Linux 中運(yùn)行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲(chǔ)小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎(chǔ)操作-- 運(yùn)算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認(rèn)證信息的處理
- Spring Security之認(rèn)證過(guò)濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設(shè)
- maven:解決release錯(cuò)誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實(shí)現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務(wù)發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡(jiǎn)單動(dòng)態(tài)字符串(SD
- arthas操作spring被代理目標(biāo)對(duì)象命令
- Spring中的單例模式應(yīng)用詳解
- 聊聊消息隊(duì)列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠(yuǎn)程分支