golang并发安全-sync.Once

本文详细解释了Go语言中的sync.Once同步原语,如何确保函数在并发环境下的唯一执行,以及它的工作原理、示例、限制和与init函数的区别。

什么是sync.Once

sync.Once 是 Go 语言中的一种同步原语,用于确保某个操作或函数在并发环境下只被执行一次。它只有一个导出的方法,即 Do,该方法接收一个函数参数。在 Do 方法被调用后,该函数将被执行,而且只会执行一次,即使在多个协程同时调用的情况下也是如此。

例子

func main() {
	var once sync.Once
	for i := 0; i < 5; i++ {
		go func(i int) {
			fun1 := func() {
				fmt.Printf("i:=%d\n", i)
			}
			once.Do(fun1)
		}(i)
	}
	time.Sleep(1 * time.Second)
}

无论执行多少次,仅返回 i:=0

看看源码

//Once 是一个只执行一次的对象。
//Once 首次使用后不得复制一次。
type Once struct {
    //done表示动作是否已执行。
    // 它在结构中是第一个,因为它在hot路径中使用。
    // 热路径在每个调用站点都是内联的
    done uint32
    m    Mutex
}

唯一的对外开放函数(Do)

func (o *Once) Do(f func()) {
   //one为0则表示未执行过,调用doSlow()方法初始化
	if atomic.LoadUint32(&o.done) == 0 {
		// Outlined slow-path to allow inlining of the fast-path.
		o.doSlow(f)
	}
}

Do(f) 被调用多次,即使f在每次调用中都有不同的值,只有第一次调用才会调用f。

func (o *Once) doSlow(f func()) {
	o.m.Lock()
	defer o.m.Unlock()
	if o.done == 0 { // 双重检查,避免 f 已被执行过
		defer atomic.StoreUint32(&o.done, 1)
		f()
	}
}

只有done == 0 才会被释放,并且使用defer保证 f()执行完会讲done置1

我们说说Once一些其他的事吧

不要嵌套 同一个once

func main() {
	var once sync.Once
	for i := 0; i < 5; i++ {
		once.Do(func() {
			once.Do(func() {
				fmt.Println(1)
			})
		})
	}
	time.Sleep(1 * time.Second)
}

直接报错:

作者在代码中提示我们:因为在对f的一个调用返回之前,不会返回对Do的调用,所以如果f导致调用Do,它将死锁。

解释 atomic.CompareAndSwapUint32 为什么不行

作者提到了 atomic.CompareAndSwapUint32 是存在问题:给定两个同时调用,cas的获胜者将调用f,第二个将立即返回,而无需等待第一个对f的调用完成。

func (o *Once) Do(f func()) {
	if atomic.CompareAndSwapUint32(&o.done, 0, 1) {
		f()
	}
}

虽然 cas 保证了同一时刻只有一个请求进入 if 判断执行 f()。但是其它的请求却没有等待 f() 执行完成就立即返回了。那么用户端在执行 once.Do 返回之后其实就可能存在 f() 还未完成,就会出现意料之外的错误。如下面例子

type OnceSelf struct {
	done uint32
}

func (o *OnceSelf) Do(f func()) {

	if atomic.CompareAndSwapUint32(&o.done, 0, 1) {f() }
}

func main() {
	var b map[string]int
	var once OnceSelf
	go func() {
		once.Do(func() {
			b = make(map[string]int, 10)
		})
	}()
	b["1"] = 1
	fmt.Println(b)

}

会存在问题

init 和 once区别

  • init 函数是在文件包首次被加载的时候执行,且只执行一次
  • sync.Onc 是在代码运行中需要的时候执行,且只执行一次

链接

what-is-fast-path-slow-path-hot-path

what-does-hot-path-mean-in-the-context-of-sync-once

在 Go 语言中,`sync.Once` 是一个用于确保某个函数在并发环境中仅执行一次的结构体。它特别适用于一次性初始化操作,例如初始化配置、连接池或全局变量的初始化等场景。以下是一个典型的使用示例: ```go package main import ( "fmt" "sync" "time" ) var once sync.Once var onceBody = func() { fmt.Println("Only once") } func main() { for i := 0; i < 5; i++ { go func(i int) { once.Do(onceBody) fmt.Println("i=", i) }(i) } time.Sleep(time.Second) // 睡眠 1s 等待 go 程执行完,注意睡眠时间不能太短。 } ``` 上述代码中,`once.Do(onceBody)` 确保了 `onceBody` 函数在整个程序生命周期中仅执行一次,即使多个 goroutine 同时调用它 [^5]。这在并发编程中非常有用,可以避免重复执行初始化逻辑。 ### 单例模式实现 `sync.Once` 常用于实现单例模式。例如,一个简单的数据库连接池可以通过 `sync.Once` 来确保仅初始化一次: ```go type Database struct { conn string } var db *Database var once sync.Once func GetDatabaseInstance() *Database { once.Do(func() { db = &Database{conn: "connected"} fmt.Println("Database instance created") }) return db } func main() { var wg sync.WaitGroup for i := 0; i < 5; i++ { wg.Add(1) go func(i int) { defer wg.Done() instance := GetDatabaseInstance() fmt.Printf("Instance %d: %v\n", i, instance) }(i) } wg.Wait() } ``` 在上述代码中,`GetDatabaseInstance()` 函数确保了 `db` 仅被初始化一次,无论多少个 goroutine 调用它 [^1]。 ### 最佳实践 - **避免在 `once.Do()` 中执行耗时操作**:虽然 `once.Do()` 保证了函数仅执行一次,但如果其中的操作非常耗时,可能会导致其他 goroutine 阻塞等待。 - **使用 `sync.Once` 替代全局锁**:当只需要确保某个操作执行一次时,使用 `sync.Once` 比全局锁更高效。 - **适用于初始化逻辑**:`sync.Once` 是初始化配置、连接池、资源加载等场景的理想选择 [^2]。 ### 与 `init` 函数的比较 `sync.Once` 的作用与 `init` 函数类似,但它们的使用时机不同: - `init` 函数在包首次被加载时执行,且只执行一次。 - `sync.Once` 在代码运行期间需要时执行,且只执行一次 [^3]。 因此,如果初始化逻辑依赖于运行时条件或需要延迟加载,`sync.Once` 是更合适的选择。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

木子林_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值