Go并發(fā)編程之sync.Once使用實(shí)例詳解
一.序
單從庫(kù)名大概就能猜出其作用。sync.Once使用起來(lái)很簡(jiǎn)單, 下面是一個(gè)簡(jiǎn)單的使用案例
package main
import (
"fmt"
"sync"
)
func main() {
var (
once sync.Once
wg sync.WaitGroup
)
for i := 0; i < 10; i++ {
wg.Add(1)
// 這里要注意講i顯示的當(dāng)參數(shù)傳入內(nèi)部的匿名函數(shù)
go func(i int) {
defer wg.Done()
// fmt.Println("once", i)
once.Do(func() {
fmt.Println("once", i)
})
}(i)
}
wg.Wait()
fmt.Printf("over")
}
輸出:
❯ go run ./demo.go
once 9
測(cè)試如果不添加once.Do 這段代碼,則會(huì)輸出如下結(jié)果,并且每次執(zhí)行的輸出都不一樣。
once 9
once 0
once 3
once 6
once 4
once 1
once 5
once 2
once 7
once 8
從兩次輸出不同,我們可以得知 sync.Once的作用是:保證傳入的函數(shù)只執(zhí)行一次
二. 源碼分析
2.1結(jié)構(gòu)體
Once的結(jié)構(gòu)體如下
type Once struct {
done uint32
m Mutex
}
每一個(gè) sync.Once 結(jié)構(gòu)體中都只包含一個(gè)用于標(biāo)識(shí)代碼塊是否執(zhí)行過(guò)的 done 以及一個(gè)互斥鎖 sync.Mutex
2.2 接口
sync.Once.Do 是 sync.Once 結(jié)構(gòu)體對(duì)外唯一暴露的方法,該方法會(huì)接收一個(gè)入?yún)榭盏暮瘮?shù):
- 如果傳入的函數(shù)已經(jīng)執(zhí)行過(guò),會(huì)直接返回
- 如果傳入的函數(shù)沒(méi)有執(zhí)行過(guò), 會(huì)調(diào)用
sync.Once.doSlow執(zhí)行傳入的參數(shù)
func (o *Once) Do(f func()) {
// Note: Here is an incorrect implementation of Do:
//
// if atomic.CompareAndSwapUint32(&o.done, 0, 1) {
// f()
// }
//
// Do guarantees that when it returns, f has finished.
// This implementation would not implement that guarantee:
// given two simultaneous calls, the winner of the cas would
// call f, and the second would return immediately, without
// waiting for the first's call to f to complete.
// This is why the slow path falls back to a mutex, and why
// the atomic.StoreUint32 must be delayed until after f returns.
if atomic.LoadUint32(&o.done) == 0 {
// Outlined slow-path to allow inlining of the fast-path.
o.doSlow(f)
}
}
代碼注釋中特別給了一個(gè)說(shuō)明: 很容易犯錯(cuò)的一種實(shí)現(xiàn)
if atomic.CompareAndSwapUint32(&o.done, 0, 1) {
f()
}
如果這么實(shí)現(xiàn)最大的問(wèn)題是,如果并發(fā)調(diào)用,一個(gè) goroutine 執(zhí)行,另外一個(gè)不會(huì)等正在執(zhí)行的這個(gè)成功之后返回,而是直接就返回了,這就不能保證傳入的方法一定會(huì)先執(zhí)行一次了
正確的實(shí)現(xiàn)方式
if atomic.LoadUint32(&o.done) == 0 {
// Outlined slow-path to allow inlining of the fast-path.
o.doSlow(f)
}
會(huì)先判斷 done 是否為 0,如果不為 0 說(shuō)明還沒(méi)執(zhí)行過(guò),就進(jìn)入 doSlow
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 當(dāng)中使用了互斥鎖來(lái)保證只會(huì)執(zhí)行一次
具體的邏輯
- 為當(dāng)前Goroutine獲取互斥鎖
- 執(zhí)行傳入的無(wú)入?yún)⒑瘮?shù);
- 運(yùn)行延遲函數(shù), 將成員變量
done更新為1
三. 使用場(chǎng)景案例
3.1 單例模式
原子操作配合互斥鎖可以實(shí)現(xiàn)非常高效的單件模式?;コ怄i的代價(jià)比普通整數(shù)的原子讀寫高很多,在性能敏感的地方可以增加一個(gè)數(shù)字型的標(biāo)志位,通過(guò)原子檢測(cè)標(biāo)志位狀態(tài)降低互斥鎖的使用次數(shù)來(lái)提高性能。
type singleton struct {}
var (
instance *singleton
initialized uint32
mu sync.Mutex
)
func Instance() *singleton {
if atomic.LoadUint32(&initialized) == 1 {
return instance
}
mu.Lock()
defer mu.Unlock()
if instance == nil {
defer atomic.StoreUint32(&initialized, 1)
instance = &singleton{}
}
return instance
}
而使用sync.Once能更簡(jiǎn)單實(shí)現(xiàn)單例模式
type singleton struct {}
var (
instance *singleton
once sync.Once
)
func Instance() *singleton {
once.Do(func() {
instance = &singleton{}
})
return instance
}
3.2 加載配置文件示例
延遲一個(gè)開銷很大的初始化操作到真正用到它的時(shí)候再執(zhí)行是一個(gè)很好的實(shí)踐。因?yàn)轭A(yù)先初始化一個(gè)變量(比如在init函數(shù)中完成初始化)會(huì)增加程序的啟動(dòng)耗時(shí),而且有可能實(shí)際執(zhí)行過(guò)程中這個(gè)變量沒(méi)有用上,那么這個(gè)初始化操作就不是必須要做的。我們來(lái)看一個(gè)例子:
var icons map[string]image.Image
func loadIcons() {
icons = map[string]image.Image{
"left": loadIcon("left.png"),
"up": loadIcon("up.png"),
"right": loadIcon("right.png"),
"down": loadIcon("down.png"),
}
}
// Icon 被多個(gè)goroutine調(diào)用時(shí)不是并發(fā)安全的
// 因?yàn)閙ap類型本就不是類型安全數(shù)據(jù)結(jié)構(gòu)
func Icon(name string) image.Image {
if icons == nil {
loadIcons()
}
return icons[name]
}
多個(gè)goroutine并發(fā)調(diào)用Icon函數(shù)時(shí)不是并發(fā)安全的,編譯器和CPU可能會(huì)在保證每個(gè)goroutine都滿足串行一致的基礎(chǔ)上自由地重排訪問(wèn)內(nèi)存的順序。loadIcons函數(shù)可能會(huì)被重排為以下結(jié)果:
func loadIcons() {
icons = make(map[string]image.Image)
icons["left"] = loadIcon("left.png")
icons["up"] = loadIcon("up.png")
icons["right"] = loadIcon("right.png")
icons["down"] = loadIcon("down.png")
}
在這種情況下就會(huì)出現(xiàn)即使判斷了icons不是nil也不意味著變量初始化完成了??紤]到這種情況,我們能想到的辦法就是添加互斥鎖,保證初始化icons的時(shí)候不會(huì)被其他的goroutine操作,但是這樣做又會(huì)引發(fā)性能問(wèn)題。
可以使用sync.Once 改造代碼
var icons map[string]image.Image
var loadIconsOnce sync.Once
func loadIcons() {
icons = map[string]image.Image{
"left": loadIcon("left.png"),
"up": loadIcon("up.png"),
"right": loadIcon("right.png"),
"down": loadIcon("down.png"),
}
}
// Icon 是并發(fā)安全的,并且保證了在代碼運(yùn)行的時(shí)候才會(huì)加載配置
func Icon(name string) image.Image {
loadIconsOnce.Do(loadIcons)
return icons[name]
}
這樣設(shè)計(jì)就能保證初始化操作的時(shí)候是并發(fā)安全的并且初始化操作也不會(huì)被執(zhí)行多次。
四.總結(jié)
作為用于保證函數(shù)執(zhí)行次數(shù)的 sync.Once 結(jié)構(gòu)體,它使用互斥鎖和 sync/atomic 包提供的方法實(shí)現(xiàn)了某個(gè)函數(shù)在程序運(yùn)行期間只能執(zhí)行一次的語(yǔ)義。在使用該結(jié)構(gòu)體時(shí),我們也需要注意以下的問(wèn)題:
sync.Once.Do方法中傳入的函數(shù)只會(huì)被執(zhí)行一次,哪怕函數(shù)中發(fā)生了 panic;- 兩次調(diào)用
sync.Once.Do方法傳入不同的函數(shù)只會(huì)執(zhí)行第一次調(diào)傳入的函數(shù);
五. 參考
- https://lailin.xyz/post/go-training-week3-once.html
- https://www.topgoer.cn/docs/gozhuanjia/chapter055.2-waitgroup
- https://www.topgoer.com/并發(fā)編程/sync.html
- https://chai2010.cn/advanced-go-programming-book/ch1-basic/ch1-05-mem.html
到此這篇關(guān)于Go并發(fā)編程--sync.Once的文章就介紹到這了,更多相關(guān)Go并發(fā)編程內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Go語(yǔ)言標(biāo)準(zhǔn)庫(kù)sync.Once使用場(chǎng)景及性能優(yōu)化詳解
- golang使用sync.Once實(shí)現(xiàn)懶加載的用法和坑點(diǎn)詳解
- golang中sync.Once只執(zhí)行一次的原理解析
- Golang并發(fā)利器sync.Once的用法詳解
- go并發(fā)利器sync.Once使用示例詳解
- go?sync.Once實(shí)現(xiàn)高效單例模式詳解
- Golang基于sync.Once實(shí)現(xiàn)單例的操作代碼
- 一文解析 Golang sync.Once 用法及原理
- Go語(yǔ)言并發(fā)編程 sync.Once
- 深入理解go sync.Once的具體使用
相關(guān)文章
完美解決beego 根目錄不能訪問(wèn)靜態(tài)文件的問(wèn)題
下面小編就為大家?guī)?lái)一篇完美解決beego 根目錄不能訪問(wèn)靜態(tài)文件的問(wèn)題。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-06-06
如何使用go實(shí)現(xiàn)創(chuàng)建WebSocket服務(wù)器
文章介紹了如何使用Go語(yǔ)言和gorilla/websocket庫(kù)創(chuàng)建一個(gè)簡(jiǎn)單的WebSocket服務(wù)器,并實(shí)現(xiàn)商品信息的實(shí)時(shí)廣播,感興趣的朋友一起看看吧2024-11-11
go 判斷兩個(gè) slice/struct/map 是否相等的實(shí)例
這篇文章主要介紹了go 判斷兩個(gè) slice/struct/map 是否相等的實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-12-12
Golang中基于HTTP協(xié)議的網(wǎng)絡(luò)服務(wù)
HTTP協(xié)議是基于TCP/IP協(xié)議棧的,并且它也是一個(gè)面向普通文本的協(xié)議。這篇文章主要詳細(xì)介紹了Golang中基于HTTP協(xié)議的網(wǎng)絡(luò)服務(wù),感興趣的小伙伴可以借鑒一下2023-04-04
深入探討Go語(yǔ)言中的預(yù)防性接口為什么是不必要的
在Go語(yǔ)言中,有一種從其他語(yǔ)言帶來(lái)的常見模式:預(yù)防性接口,雖然這種模式在?Java?等語(yǔ)言中很有價(jià)值,但在Go中往往會(huì)成為反模式,本文我們就來(lái)深入探討一下原因2025-01-01
Windows系統(tǒng)中搭建Go語(yǔ)言開發(fā)環(huán)境圖文詳解
GoLand?是?JetBrains?公司推出的商業(yè)?Go?語(yǔ)言集成開發(fā)環(huán)境(IDE),這篇文章主要介紹了Windows系統(tǒng)中搭建Go語(yǔ)言開發(fā)環(huán)境詳解,需要的朋友可以參考下2022-10-10
詳解Go如何基于現(xiàn)有的context創(chuàng)建新的context
在?Golang?中,context?包提供了創(chuàng)建和管理上下文的功能,那么在GO語(yǔ)言中如何基于現(xiàn)有的context創(chuàng)建新的context,下面小編就來(lái)和大家詳細(xì)聊聊2024-01-01

