網站首頁 編程語言 正文
正文
在很多情況下,我們可能需要控制某一段代碼只執行一次,比如做某些初始化操作,如初始化數據庫連接等。 對于這種場景,go 為我們提供了 sync.Once
對象,它保證了某個動作只被執行一次。 當然我們也是可以自己通過 Mutex
實現 sync.Once
的功能,但是相比來說繁瑣了那么一點, 因為我們不僅要自己去控制鎖,還要通過一個標識來標志是否已經執行過。
Once 的實現
Once
的實現非常簡單,如下,就只有 20 來行代碼,但里面包含了 go 并發、同步的一些常見處理方法。
package sync import ( "sync/atomic" ) type Once struct { done uint32 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() } }
簡要說明:
-
done
字段指示了操作是否已執行,也就是我們傳遞給Do
的函數是否已經被執行。 -
Do
方法接收一個函數參數,這個函數參數只會被執行一次。 -
Once
內部是通過Mutex
來實現不同協程之間的同步的。
使用示例
在下面的例子中,once.Do(test)
被執行了 3 次,但是最終 test
只被執行了一次。
package sync import ( "fmt" "sync" "testing" ) var once sync.Once var a = 0 func test() { a++ } func TestOnce(t *testing.T) { var wg sync.WaitGroup wg.Add(3) for i := 0; i < 3; i++ { go func() { // once.Do 會調用 3 次,但最終只會執行一次 once.Do(test) wg.Done() }() } wg.Wait() fmt.Println(a) // 1 }
Once 的一些工作機制
-
Once
的Do
方法可以保證,在多個 goroutine 同時執行Do
方法的時候, 在第一個搶占到Do
執行權的 goroutine 執行返回之前,其他 goroutine 都會阻塞在Once.Do
的調用上, 只有第一個Do
調用返回的時候,其他 goroutine 才可以繼續執行下去,并且其他所有的 goroutine 不會再執行傳遞給Do
的函數。(如果是初始化的場景,這可以避免尚未初始化完成就執行其他的操作) - 如果
Once.Do
發生panic
的時候,傳遞給Do
的函數依然被標記為已完成。后續對Do
的調用也不會再執行傳給Do
的函數參數。 - 我們不能簡單地通過
atomic.CompareAndSwapUint32
來決定是否執行f()
,因為在多個 goroutine 同時執行的時候,它無法保證f()
只被執行一次。所以Once
里面用了Mutex
,這樣就可以有效地保護臨界區。
// 錯誤實現,這不能保證 f 只被執行一次 if atomic.CompareAndSwapUint32(&o.done, 0, 1) { f() }
-
Once.Do
的函數參數是沒有參數的,如果我們需要傳遞一些參數,可以再對f
做一層包裹。
config.once.Do(func() { config.init(filename) })
Once 詳解
hotpath
這里說的 hotpath
指的是 Once
里的第一個字段 done
:
type Once struct { // hotpath done uint32 m Mutex }
Once
結構體的第一個字段是 done
,這是因為 done
的訪問是遠遠大于 Once
中另外一個字段 m
的, 放在第一個字段中,編譯器就可以做一些優化,因為結構體的地址其實就是結構體第一個字段的地址, 這樣一來,在訪問 done
字段的時候,就不需要通過結構體地址 + 偏移量的方式來訪問, 這在一定程度上提高了性能。
結構體地址計算示例:
type person struct { name string age int } func TestStruct(t *testing.T) { var p = person{ name: "foo", age: 10, } // p 和 p.name 的地址相同 // 0xc0000100a8, 0xc0000100a8 fmt.Printf("%p, %p\n", &p, &p.name) // p.age 的地址 // 0xc0000100b8 fmt.Printf("%p\n", &p.age) // p.age 的地址也可以通過:結構體地址 + age 字段偏移量 計算得出。 // 0xc0000100b8 fmt.Println(unsafe.Add(unsafe.Pointer(&p), unsafe.Offsetof(p.age))) }
atomic.LoadUint32
func (o *Once) Do(f func()) { if atomic.LoadUint32(&o.done) == 0 { o.doSlow(f) } }
在 Do
方法中,是通過 atomic.LoadUint32
的方式來判斷 done
是否等于 0 的, 這是因為,如果直接使用 done == 0
的方式的話,就有可能導致在 doSlow
里面對 done
設置為 1 之后, 在 Do
方法里面無法正常觀測到。因此用了 atomic.LoadUint32
。
而在 doSlow
里面是可以通過 done == 0
來判斷的,這是因為 doSlow
里面已經通過 Mutex
保護起來了。 唯一設置 done = 1
的地方就在臨界區里面,所以 doSlow
里面通過 done == 0
來判斷是完全沒有問題的。
atomic.StoreUint32
func (o *Once) doSlow(f func()) { o.m.Lock() defer o.m.Unlock() if o.done == 0 { defer atomic.StoreUint32(&o.done, 1) f() } }
在 doSlow
方法中,設置 done
為 1 也是通過 atomic.StoreUint32
來設置的。 這樣就可以保證在設置了 done
為 1 之后,可以及時被其他 goroutine 看到。
Mutex
doSlow
的實現里面,最終還是要通過 Mutex
來保護臨界區, 通過 Mutex
可以實現 f
只被執行一次,并且其他的 goroutine 都可以使用這一次 f
的執行結果。 因為其他 goroutine 在第一次 f
調用未返回之前,都阻塞在獲取 Mutex
鎖的地方, 當它們獲取到 Mutex
鎖的時候,得以繼續往下執行,但這個時候 f
已經執行完畢了, 所以當它們獲取到 Mutex
鎖之后其實什么也沒有干。
但是它們的阻塞狀態被解除了,可以繼續往下執行。
總結
-
Once
保證了傳入的函數只會執行一次,這常常用在一些初始化的場景、或者單例模式。 -
Once
可以保證所有對Do
的并發調用都是安全的,所有對Once.Do
調用之后的操作,一定會在第一次對f
調用之后執行。(沒有獲取到f
執行權的 goroutine 會阻塞) - 即使
Once.Do
里面的f
出現了panic
,后續也不會再次調用f
。
原文鏈接:https://juejin.cn/post/7181328682093379621
相關推薦
- 2022-08-18 詳解如何從Matlab中導出清晰的結果圖片_C 語言
- 2022-12-06 Android?LayerDrawable超詳細講解_Android
- 2021-12-16 jquery+swiper組件實現時間軸滑動年份tab切換效果_jquery
- 2022-07-11 Jenkins修改默認主目錄
- 2022-09-26 數據結構---圖、十字鏈表及其代碼實現
- 2023-05-09 C語言qsort函數用冒泡排序實現過程詳解_C 語言
- 2022-07-18 linux引導和計劃任務
- 2022-08-11 Redis實現主從復制方式(Master&Slave)_Redis
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支