golang中sync.Once只執(zhí)行一次的原理解析
背景
在某些場景下,我們希望某個操作或者函數(shù)僅被執(zhí)行一次,比如單例模式的初始化,一些資源配置的加載等。
golang中的sync.Once就實現(xiàn)了這個功能,Once只對外提供一個Do方法,Do方法只接收一個函數(shù)參數(shù),它可以保證并發(fā)場景下,多次對Do方法進行調(diào)用時,參數(shù)對應(yīng)的函數(shù)只被執(zhí)行一次
快速入門
定義一個f1函數(shù),同時開啟10個并發(fā),通過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只對外提供一個Do方法 func (o *Once) Do(f func()) {}
- Once內(nèi)部有兩個字段:done和m
- done用來表示傳入的函數(shù)是否已執(zhí)行完成,未執(zhí)行和執(zhí)行中時,done=0,執(zhí)行完成時,done=1
- m互斥鎖,用來保證并發(fā)調(diào)用時,傳入的函數(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() } }
- 先通過atomic.LoadUint32(&o.done) == 0快速判斷,傳入的函數(shù)參數(shù),是否已經(jīng)執(zhí)行完成。若done=0,表示函數(shù)未執(zhí)行或正在執(zhí)行中;若done=1,表示函數(shù)已執(zhí)行完成,則快速返回
- 通過m互斥鎖進行加鎖,保證并發(fā)安全
- 通過o.done == 0二次確認,傳入的函數(shù)參數(shù)是否已經(jīng)被執(zhí)行。若此時done=0,因為上一步已經(jīng)通過m進行了加鎖,所以可以保證的是,傳入的函數(shù)還沒有被執(zhí)行,此時執(zhí)行函數(shù)后,把done改為1即可;若此時done!=0,則表示在等待鎖的期間,已經(jīng)有其他goroutine成功執(zhí)行了函數(shù),此時直接返回即可
注意點一:同一個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不會執(zhí)行 once.Do(f2) }
定義f1和f2兩個函數(shù),通過同一個Once來執(zhí)行時,只能保證f1函數(shù)被執(zhí)行一次
Once.Do保證的是第一個傳入的函數(shù)參數(shù)只被執(zhí)行一次,不是保證每一個傳入的函數(shù)參數(shù)都只被執(zhí)行一次,同一個Once不能復(fù)用,如果想要f1和f2都只被執(zhí)行一次,可以初始化兩個Once
注意點二:錯誤實現(xiàn)
if atomic.CompareAndSwapUint32(&o.done, 0, 1) { f() }
為什么通過CAS來實現(xiàn)是錯誤的?
因為CAS只能保證函數(shù)被執(zhí)行一次,但是不能保證f()還在執(zhí)行時,其他goroutine等待其執(zhí)行完成后再返回。這個很重要,當(dāng)我們傳入的函數(shù)是比較耗時的操作,比如和db建立連接等,就必須等待函數(shù)執(zhí)行完成再返回,不然就會出現(xiàn)一些未知的操作
注意點三:atomic.LoadUint32(&o.done) == 0和atomic.StoreUint32(&o.done, 1)
為什么使用atomic.LoadUint32(&o.done) == 0來判斷,而不是使用o.done == 0來判斷
為了防止發(fā)生數(shù)據(jù)競爭,使用o.done == 0來判斷,會發(fā)生數(shù)據(jù)競爭(Data Race)
數(shù)據(jù)競爭問題是指至少存在兩個線程/協(xié)程去讀寫某個共享內(nèi)存,其中至少一個線程/協(xié)程對其共享內(nèi)存進行寫操作
多個線程/協(xié)程同時對共享內(nèi)存的進行寫操作時,在寫的過程中,其他的線程/協(xié)程讀到數(shù)據(jù)是內(nèi)存數(shù)據(jù)中非正確預(yù)期的
驗證數(shù)據(jù)競爭問題:
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()) { // 原來: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 { // 原來: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í)行一次的原理解析的詳細內(nèi)容,更多關(guān)于golang sync.Once執(zhí)行一次的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
深入分析Go?實現(xiàn)?MySQL?數(shù)據(jù)庫事務(wù)
本文深入分析了Go語言實現(xiàn)MySQL數(shù)據(jù)庫事務(wù)的原理和實現(xiàn)方式,包括事務(wù)的ACID特性、事務(wù)的隔離級別、事務(wù)的實現(xiàn)方式等。同時,本文還介紹了Go語言中的事務(wù)處理機制和相關(guān)的API函數(shù),以及如何使用Go語言實現(xiàn)MySQL數(shù)據(jù)庫事務(wù)。2023-06-06Go實現(xiàn)數(shù)據(jù)脫敏的方案設(shè)計
在一些常見的業(yè)務(wù)場景中可能涉及到用戶的手機號,銀行卡號等敏感數(shù)據(jù),對于這部分的數(shù)據(jù)經(jīng)常需要進行數(shù)據(jù)脫敏處理,就是將此部分數(shù)據(jù)隱私化,防止數(shù)據(jù)泄露,所以本文給大家介紹了Go實現(xiàn)數(shù)據(jù)脫敏的方案設(shè)計,需要的朋友可以參考下2024-05-05