golang中sync.Once只执行一次的原理解析
背景
在某些场景下,我们希望某个操作或者函数仅被执行一次,比如单例模式的初始化,一些资源配置的加载等。
golang中的sync.Once就实现了这个功能,Once只对外提供一个Do方法,Do方法只接收一个函数参数,它可以保证并发场景下,多次对Do方法进行调用时,参数对应的函数只被执行一次
快速入门
定义一个f1函数,同时开启10个并发,通过Once提供的Do方法去执行f1函数,Once可以保证f1函数只被执行一次
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内部有两个字段:done和m
- done用来表示传入的函数是否已执行完成,未执行和执行中时,done=0,执行完成时,done=1
- m互斥锁,用来保证并发调用时,传入的函数只被执行一次
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快速判断,传入的函数参数,是否已经执行完成。若done=0,表示函数未执行或正在执行中;若done=1,表示函数已执行完成,则快速返回
- 通过m互斥锁进行加锁,保证并发安全
- 通过o.done == 0二次确认,传入的函数参数是否已经被执行。若此时done=0,因为上一步已经通过m进行了加锁,所以可以保证的是,传入的函数还没有被执行,此时执行函数后,把done改为1即可;若此时done!=0,则表示在等待锁的期间,已经有其他goroutine成功执行了函数,此时直接返回即可
注意点一:同一个Once不能复用
func TestOnce(t *testing.T) { once := sync.Once{} f1 := func() { fmt.Println("f1 func") } f2 := func() { fmt.Println("f2 func") } // f1执行成功 once.Do(f1) // f2不会执行 once.Do(f2) }
定义f1和f2两个函数,通过同一个Once来执行时,只能保证f1函数被执行一次
Once.Do保证的是第一个传入的函数参数只被执行一次,不是保证每一个传入的函数参数都只被执行一次,同一个Once不能复用,如果想要f1和f2都只被执行一次,可以初始化两个Once
注意点二:错误实现
if atomic.CompareAndSwapUint32(&o.done, 0, 1) { f() }
为什么通过CAS来实现是错误的?
因为CAS只能保证函数被执行一次,但是不能保证f()还在执行时,其他goroutine等待其执行完成后再返回。这个很重要,当我们传入的函数是比较耗时的操作,比如和db建立连接等,就必须等待函数执行完成再返回,不然就会出现一些未知的操作
注意点三:atomic.LoadUint32(&o.done) == 0和atomic.StoreUint32(&o.done, 1)
为什么使用atomic.LoadUint32(&o.done) == 0来判断,而不是使用o.done == 0来判断
为了防止发生数据竞争,使用o.done == 0来判断,会发生数据竞争(Data Race)
数据竞争问题是指至少存在两个线程/协程去读写某个共享内存,其中至少一个线程/协程对其共享内存进行写操作
多个线程/协程同时对共享内存的进行写操作时,在写的过程中,其他的线程/协程读到数据是内存数据中非正确预期的
验证数据竞争问题:
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() } }
执行命令:
go run -race main.go
执行结果:
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只执行一次的原理解析的详细内容,更多关于golang sync.Once执行一次的资料请关注脚本之家其它相关文章!
最新评论