golang中sync.Once只執(zhí)行一次的原理解析
背景
在某些場(chǎng)景下,我們希望某個(gè)操作或者函數(shù)僅被執(zhí)行一次,比如單例模式的初始化,一些資源配置的加載等。
golang中的sync.Once就實(shí)現(xiàn)了這個(gè)功能,Once只對(duì)外提供一個(gè)Do方法,Do方法只接收一個(gè)函數(shù)參數(shù),它可以保證并發(fā)場(chǎng)景下,多次對(duì)Do方法進(jìn)行調(diào)用時(shí),參數(shù)對(duì)應(yīng)的函數(shù)只被執(zhí)行一次
快速入門
定義一個(gè)f1函數(shù),同時(shí)開啟10個(gè)并發(fā),通過(guò)Once提供的Do方法去執(zhí)行f1函數(shù),Once可以保證f1函數(shù)只被執(zhí)行一次
func TestOnce(t *testing.T) {
once := sync.Once{}
f1 := func() {
fmt.Println("f1 func")
}
wg := sync.WaitGroup{}
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
once.Do(f1)
}()
}
wg.Wait()
}源碼分析
golang版本:1.18.2
源碼路徑:src/sync/Once.go
// Once is an object that will perform exactly one action.
//
// A Once must not be copied after first use.
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
}
// Once只對(duì)外提供一個(gè)Do方法
func (o *Once) Do(f func()) {}- Once內(nèi)部有兩個(gè)字段:done和m
- done用來(lái)表示傳入的函數(shù)是否已執(zhí)行完成,未執(zhí)行和執(zhí)行中時(shí),done=0,執(zhí)行完成時(shí),done=1
- m互斥鎖,用來(lái)保證并發(fā)調(diào)用時(shí),傳入的函數(shù)只被執(zhí)行一次
Do()
// 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()) {
// 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)
}
}
func (o *Once) doSlow(f func()) {
o.m.Lock()
defer o.m.Unlock()
if o.done == 0 {
defer atomic.StoreUint32(&o.done, 1)
f()
}
}- 先通過(guò)atomic.LoadUint32(&o.done) == 0快速判斷,傳入的函數(shù)參數(shù),是否已經(jīng)執(zhí)行完成。若done=0,表示函數(shù)未執(zhí)行或正在執(zhí)行中;若done=1,表示函數(shù)已執(zhí)行完成,則快速返回
- 通過(guò)m互斥鎖進(jìn)行加鎖,保證并發(fā)安全
- 通過(guò)o.done == 0二次確認(rèn),傳入的函數(shù)參數(shù)是否已經(jīng)被執(zhí)行。若此時(shí)done=0,因?yàn)樯弦徊揭呀?jīng)通過(guò)m進(jìn)行了加鎖,所以可以保證的是,傳入的函數(shù)還沒(méi)有被執(zhí)行,此時(shí)執(zhí)行函數(shù)后,把done改為1即可;若此時(shí)done!=0,則表示在等待鎖的期間,已經(jīng)有其他goroutine成功執(zhí)行了函數(shù),此時(shí)直接返回即可
注意點(diǎn)一:同一個(gè)Once不能復(fù)用
func TestOnce(t *testing.T) {
once := sync.Once{}
f1 := func() {
fmt.Println("f1 func")
}
f2 := func() {
fmt.Println("f2 func")
}
// f1執(zhí)行成功
once.Do(f1)
// f2不會(huì)執(zhí)行
once.Do(f2)
}定義f1和f2兩個(gè)函數(shù),通過(guò)同一個(gè)Once來(lái)執(zhí)行時(shí),只能保證f1函數(shù)被執(zhí)行一次
Once.Do保證的是第一個(gè)傳入的函數(shù)參數(shù)只被執(zhí)行一次,不是保證每一個(gè)傳入的函數(shù)參數(shù)都只被執(zhí)行一次,同一個(gè)Once不能復(fù)用,如果想要f1和f2都只被執(zhí)行一次,可以初始化兩個(gè)Once
注意點(diǎn)二:錯(cuò)誤實(shí)現(xiàn)
if atomic.CompareAndSwapUint32(&o.done, 0, 1) {
f()
}為什么通過(guò)CAS來(lái)實(shí)現(xiàn)是錯(cuò)誤的?
因?yàn)镃AS只能保證函數(shù)被執(zhí)行一次,但是不能保證f()還在執(zhí)行時(shí),其他goroutine等待其執(zhí)行完成后再返回。這個(gè)很重要,當(dāng)我們傳入的函數(shù)是比較耗時(shí)的操作,比如和db建立連接等,就必須等待函數(shù)執(zhí)行完成再返回,不然就會(huì)出現(xiàn)一些未知的操作
注意點(diǎn)三:atomic.LoadUint32(&o.done) == 0和atomic.StoreUint32(&o.done, 1)
為什么使用atomic.LoadUint32(&o.done) == 0來(lái)判斷,而不是使用o.done == 0來(lái)判斷
為了防止發(fā)生數(shù)據(jù)競(jìng)爭(zhēng),使用o.done == 0來(lái)判斷,會(huì)發(fā)生數(shù)據(jù)競(jìng)爭(zhēng)(Data Race)
數(shù)據(jù)競(jìng)爭(zhēng)問(wèn)題是指至少存在兩個(gè)線程/協(xié)程去讀寫某個(gè)共享內(nèi)存,其中至少一個(gè)線程/協(xié)程對(duì)其共享內(nèi)存進(jìn)行寫操作
多個(gè)線程/協(xié)程同時(shí)對(duì)共享內(nèi)存的進(jìn)行寫操作時(shí),在寫的過(guò)程中,其他的線程/協(xié)程讀到數(shù)據(jù)是內(nèi)存數(shù)據(jù)中非正確預(yù)期的
驗(yàn)證數(shù)據(jù)競(jìng)爭(zhēng)問(wèn)題:
package main
import (
"fmt"
"sync"
)
func main() {
once := Once{}
var wg sync.WaitGroup
wg.Add(2)
go func() {
once.Do(print)
wg.Done()
}()
go func() {
once.Do(print)
wg.Done()
}()
wg.Wait()
fmt.Println("end")
}
func print() {
fmt.Println("qqq")
}
type Once struct {
done uint32
m sync.Mutex
}
func (o *Once) Do(f func()) {
// 原來(lái):atomic.LoadUint32(&o.done) == 0
if o.done == 0 {
o.doSlow(f)
}
}
func (o *Once) doSlow(f func()) {
o.m.Lock()
defer o.m.Unlock()
if o.done == 0 {
// 原來(lái):atomic.StoreUint32(&o.done, 1)
defer func() {
o.done = 1
}()
f()
}
}執(zhí)行命令:
go run -race main.go
執(zhí)行結(jié)果:
qqq
==================
WARNING: DATA RACE
Write at 0x00c0000bc014 by goroutine 7:
main.(*Once).doSlow.func1()
/Users/cr/Documents/golang/src/ahut.com/go/demo/main.go:44 +0x32
runtime.deferreturn()
/usr/local/go/src/runtime/panic.go:436 +0x32
main.(*Once).Do()
/Users/cr/Documents/golang/src/ahut.com/go/demo/main.go:35 +0x52
main.main.func1()
/Users/cr/Documents/golang/src/ahut.com/go/demo/main.go:13 +0x37
Previous read at 0x00c0000bc014 by goroutine 8:
main.(*Once).Do()
/Users/cr/Documents/golang/src/ahut.com/go/demo/main.go:34 +0x3c
main.main.func2()
/Users/cr/Documents/golang/src/ahut.com/go/demo/main.go:17 +0x37
Goroutine 7 (running) created at:
main.main()
/Users/cr/Documents/golang/src/ahut.com/go/demo/main.go:12 +0x136
Goroutine 8 (running) created at:
main.main()
/Users/cr/Documents/golang/src/ahut.com/go/demo/main.go:16 +0x1da
==================
end
Found 1 data race(s)
exit status 66以上就是golang中sync.Once只執(zhí)行一次的原理解析的詳細(xì)內(nèi)容,更多關(guān)于golang sync.Once執(zhí)行一次的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- Go語(yǔ)言標(biāo)準(zhǔn)庫(kù)sync.Once使用場(chǎng)景及性能優(yōu)化詳解
- golang使用sync.Once實(shí)現(xiàn)懶加載的用法和坑點(diǎn)詳解
- Golang并發(fā)利器sync.Once的用法詳解
- go并發(fā)利器sync.Once使用示例詳解
- go?sync.Once實(shí)現(xiàn)高效單例模式詳解
- Golang基于sync.Once實(shí)現(xiàn)單例的操作代碼
- 一文解析 Golang sync.Once 用法及原理
- Go并發(fā)編程之sync.Once使用實(shí)例詳解
- Go語(yǔ)言并發(fā)編程 sync.Once
- 深入理解go sync.Once的具體使用
相關(guān)文章
深入分析Go?實(shí)現(xiàn)?MySQL?數(shù)據(jù)庫(kù)事務(wù)
本文深入分析了Go語(yǔ)言實(shí)現(xiàn)MySQL數(shù)據(jù)庫(kù)事務(wù)的原理和實(shí)現(xiàn)方式,包括事務(wù)的ACID特性、事務(wù)的隔離級(jí)別、事務(wù)的實(shí)現(xiàn)方式等。同時(shí),本文還介紹了Go語(yǔ)言中的事務(wù)處理機(jī)制和相關(guān)的API函數(shù),以及如何使用Go語(yǔ)言實(shí)現(xiàn)MySQL數(shù)據(jù)庫(kù)事務(wù)。2023-06-06
基于golang時(shí)間轉(zhuǎn)換的問(wèn)題
下面小編就為大家?guī)?lái)一篇基于golang時(shí)間轉(zhuǎn)換的問(wèn)題。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-08-08
Go實(shí)現(xiàn)數(shù)據(jù)脫敏的方案設(shè)計(jì)
在一些常見(jiàn)的業(yè)務(wù)場(chǎng)景中可能涉及到用戶的手機(jī)號(hào),銀行卡號(hào)等敏感數(shù)據(jù),對(duì)于這部分的數(shù)據(jù)經(jīng)常需要進(jìn)行數(shù)據(jù)脫敏處理,就是將此部分?jǐn)?shù)據(jù)隱私化,防止數(shù)據(jù)泄露,所以本文給大家介紹了Go實(shí)現(xiàn)數(shù)據(jù)脫敏的方案設(shè)計(jì),需要的朋友可以參考下2024-05-05
Go語(yǔ)言操作Excel的實(shí)現(xiàn)示例
excelize是一個(gè)功能豐富且易于使用的Go語(yǔ)言庫(kù),它極大地簡(jiǎn)化了Excel文件的讀寫操作,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2024-12-12
Go語(yǔ)言入門教程之基礎(chǔ)語(yǔ)法快速入門
這篇文章主要介紹了Go語(yǔ)言入門教程之基礎(chǔ)語(yǔ)法快速入門,本文講解了值類型、變量、常量、循環(huán)、條件語(yǔ)句、條件枚舉等內(nèi)容,需要的朋友可以參考下2014-11-11

