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执行一次的资料请关注脚本之家其它相关文章!
相关文章
Golang 实现 Redis系列(六)如何实现 pipeline 模式的 redis 客户端
pipeline 模式的 redis 客户端需要有两个后台协程负责 tcp 通信,调用方通过 channel 向后台协程发送指令,并阻塞等待直到收到响应,本文是使用 golang 实现 redis 系列的第六篇, 将介绍如何实现一个 Pipeline 模式的 Redis 客户端。2021-07-07
解决电脑用GoLand太卡将VsCode定制成Go IDE步骤过程
这篇文章主要为大家介绍了解决电脑用GoLand太卡,将VsCode定制成Go IDE步骤过程详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪2023-11-11


最新评论