sync.Once-保证运行期间的某段代码只会执行一次

初入门径

sync.Once提供了保证某个操作只被执行一次的功能,其最常应用于单例模式之下,例如初始化系统配置、保持数据库唯一连接,以及并发访问只需要初始化一次的共享资源。

单例模式有懒汉模式 和饿汉模式两种

饿汉模式 顾名思义就是比较饥饿,所以一上来(服务启动时)就初始化。

懒汉模式 顾名思义就是偷懒,在获取实例的时候在进行初始化,但懒汉模式会有并发问题:有可能多个 goruntine 同时获取 对象都是 nil ,然后都开始创建了实例,就不满足单例模式了。解决办法是加锁

sync.Once就是懒汉模式

Go并发编程 --- sync.Once

go 复制代码
package main

import (
	"fmt"
	"sync"
)

func main() {
	var once sync.Once
	fun1 := func() {
		fmt.Println("一只老虎")
	}
	once.Do(fun1)

	fun2 := func() {
		fmt.Println("两只老虎")
	}

	once.Do(fun2)
}

输出为:

go 复制代码
一只老虎

并发调用 Do()

go 复制代码
package main

import (
	"fmt"
	"sync"
	"time"
)

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)
	}

	// 为防止主goroutine直接运行完,什么都看不到
	time.Sleep(50 * time.Millisecond)
}

在上面这段代码里,开启了5个并发的 goroutine ,不管执行多少次, 始终只打印一次. 至于 i 是多少,就看先执行的是哪个 g 了。

Once 保证只有第一次调用 Do() 方法时, 传递的 f (无参数无返回值的函数) 才会执行,并且之后不管调用的参数是否改变了,也不管f执行时是否发生了panic,之后都不再执行。

这段是官方Demo:

go 复制代码
 package main

 import (
     "fmt"
    "sync"
 )
 
 func main() {
     var once sync.Once
    onceBody := func() {
        fmt.Println("Only once")
    }
    done := make(chan bool)
    for i := 0; i < 10; i++ {
        go func() {
            once.Do(onceBody)
            done <- true
        }()
    }
    for i := 0; i < 10; i++ {
        <-done
    }
}
// 结果只打印一次:only once

源码实现

其代码实现很简洁, 从头到尾加注释不超过 70 行. 对外暴露了一个唯一接口 Do(f func()), 使用起来也很简单,可以多次调用,但是只有第一次调用Do方法时f参数才会执行,这里的f是一个无参数无返回值的函数。

点击查看 sync.Once源码:

go 复制代码
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package sync

import (
	"sync/atomic"
)

// Once is an object that will perform exactly one action.
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/x86),
	// and fewer instructions (to calculate offset) on other architectures.

    // done用来表示当前的操作是否已经被执行, 0表示还没有执行过, 1表示已经执行
    // done属性放在结构体的第一位,是因为它在hot path中使用
    // hot path在每个调用点会被内联
    // 将done放在结构体首位,像amd64/386等架构上可以允许更多的压缩指令,且在其他架构上更少的指令去计算偏移量
	done uint32
	m    Mutex
}

// 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.
//

// Do的作用主要是针对初始化且有且只能执行一次的场景。因为Do直到f返回才返回,`
// 所以如果f内再调用Do则会导致死锁
// 如果f执行过程中panic了 那么Do任务f已经执行完毕 未来再次调用不会再执行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.
        // 原子获取 done 的值,判断 done 值是否为 0,如果为 0 就调用 doSlow 方法,进行二次检查。
        // 可能会存在并发 进入slow-path
		o.doSlow(f)
	}
}

func (o *Once) doSlow(f func()) {
    // 二次检查时,持有互斥锁,保证只有一个 goroutine 执行。
	o.m.Lock()
	defer o.m.Unlock()
	if o.done == 0 { //二次判断f是否已经被执行
         // 二次检查,如果 done 的值仍为 0,则认为是第一次执行,执行参数 f,并将 done 的值设置为 1。

         //即使此时有多个 goroutine 同时进入了 doSlow 方法,因为双检查的机制,后续的 goroutine 会看到 o.done 的值为 1,也不会再次执行 f
		defer atomic.StoreUint32(&o.done, 1)
		f()
	}
}

Once 结构体中包含两个字段, 分别是 uint32 类型的 done 和 Mutex 类型的 m.

Once 实现了两个方法, 分别是 Do 和 doSlow。其中 doSlow 是一个非可导出方法,只能在内部被 Do 方法调用.

Done 方法先原子获取 done 的值,如果 done 的值为 0,则调用 doSlow 方法进行二次检查,二次检查时,持有互斥锁,保证只有一个 goroutine 执行操作.如果二次检查的结果为 0,则认为是第一次执行,程序执行函数类型的参数 f,然后将 done 的值设置为 1.


注意事项

go 复制代码
package main

import (
	"fmt"
	"sync"
)

func main() {

	panicDo()

	//nestedDo()
	nestedDo2()

}

func panicDo() {
	once := &sync.Once{}
	defer func() {
		if err := recover(); err != nil {
			once.Do(func() {
				fmt.Println("run in recover")
			})
		}
	}()
	once.Do(func() {
		panic("panic i=0")
	})
}

func nestedDo() {
	once := &sync.Once{}
	once.Do(func() {
		once.Do(func() {
			fmt.Println("test nestedDo")
		})
	})
}

func nestedDo2() {
	once1 := &sync.Once{}
	once2 := &sync.Once{}
	once1.Do(func() {
		once2.Do(func() {
			fmt.Println("test nestedDo")
		})
	})
}

详解并发编程之sync.Once的实现(附上三道面试题)

(1). sync.Once()方法中传入的函数发生了panic,重复传入还会执行吗?

执行panicDo方法,不会打印任何东西. sync.Once.Do 方法中传入的函数只会被执行一次,哪怕函数中发生了 panic;

(2). sync.Once()方法传入的函数中再次调用sync.Once()方法会有什么问题吗?

会发生死锁! 执行nestedDo方法,会报 fatal error: all goroutines are asleep - deadlock! 根据源码实现,可知在第二个do方法会一直等doshow()中锁的释放导致发生了死锁;

(3). 执行nestedDo2,会输出什么?

会打印出 test nestedDo. once1,once2是两个对象,互不影响. 所以sync.Once是使方法只执行一次对象的实现。


官方库或知名项目中的使用

net 包 系统的网络配置 就是存放在一个变量里,用sync.Once控制读写

当且仅当第一次读某个变量时,进行初始化(写操作)

变量被初始化过程中,所有读都被阻塞(读操作;当变量初始化完成后,读操作继续进行

变量仅初始化一次,初始化完成后驻留在内存里

Golang package sync 剖析(一): sync.Once


参考:

你真的了解 sync.Once 吗

深入源码分析golang之sync.Once

相关推荐
独泪了无痕3 小时前
Spring Data Redis 快速上手
spring boot·redis·后端
m0_639906947 小时前
Linux 操作系统入门:目录结构、服务排查常用命令、Shell 脚本基础
linux·运维·后端
holly就是我8 小时前
AI 编程助手为什么跑偏?用开源 ccglass 看清 Prompt、工具调用和 Token 消耗
人工智能·后端
鸿蒙开发8 小时前
共享 Schema 与 SchemaFactory
前端·后端·harmonyos
用户788477316348 小时前
小程序后端三选一:云开发、.NET WebAPI、Node,我按客户现状分了三种答案
后端
aiopencode8 小时前
局域网设备抓包怎么做?用 TraceEagle 抓电视、游戏机和 IoT 流量
后端·ios
智驭未来掌门人8 小时前
CLion 开发 Qt 项目的优势
后端
EnCi Zheng8 小时前
后端怎么把AI接口稳定安全低成本接进业务:超时重试、密钥隔离与成本熔断清单
人工智能·后端·安全·架构·api
Bazingga9 小时前
从一家酒店讲懂Docker
后端
南归北隐9 小时前
Spring AI Alibaba Graph框架实现MCP数据库查询实战
java·后端·spring ai·mcp·spring ai mcp