添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

Go并发编程-sync.once原理剖析

一、前言

有时,我们需要让某个函数只执行一次,那么如何来保证那?一般是我们自己写个原子变量来控制函数是否已经执行过,这样可行,但是需要我们自己实现这非业务逻辑的功能,其实go中提供了sync.once()方法,帮我们封装了这层操作。

二、自己实现函数只执行一次

如下代码,我们在main方法内通过循环语句开启新goroutine异步调用sayHello()方法,运行后我们会看到输出100句 hello,world。

func sayHello() {
    fmt.Println("hello,world")
func main() {
    for i := 0; i < 100; i++ {
        go sayHello()
    // go中main goroutine退出,则整个进程就退出
    //所以这里我们简单休眠,等待子goroutine执行完毕
    time.Sleep(time.Minute)

如果我们想要实现虽然调用100次sayHello()但是只输出一条hello,world,一个可能的实现是这样的:

var flag int32
func sayHello() {
    if atomic.CompareAndSwapInt32(&flag, 0, 1) {
        fmt.Println("hello,world")
func main() {
    for i := 0; i < 100; i++ {
        go sayHello()
    // go中main goroutine退出,则整个进程就退出
    //所以这里我们简单休眠,等待子goroutine执行完毕
    time.Sleep(time.Minute)

如上代码,我们创建了一个flag变量,然后再sayHello方法内使用CAS设置flag的值,由于flag的初始化值为0,所以当多个goroutine执行CAS时,只有一个goroutine可以把flag的值从0设置为1,其他的goroutine执行CAS会返回FALSE,这保证了只有一个goroutine可以打印hello,world.

上面的实现我们把CAS操作放到了sayHello方法内部了,假如其他函数也需要实现只执行一次我们还需要修改其函数内容,显然这不可取。所以我们打算抽取一个方法,让执行一次的逻辑独立出来,对所有函数都适用,一个可能的实现如下:

var flag int32
func onceCas(f func()) {
    if atomic.CompareAndSwapInt32(&flag, 0, 1) {
func main() {
    for i := 0; i < 100; i++ {
        go onceCas(sayHello)

如上代码,我们创建了onceCas(即基于CAS实现的once执行功能)函数,其入参为func(),内部则基于CAS保证函数f只执行一次。这看起来很好,确实可以解决上面的一类问题,但是下面让我们看一个例子,看看其存在的一些不足:

func onceCas(f func()) {
    if atomic.CompareAndSwapInt32(&flag, 0, 1) {
var u *user
func getInstance() {
    u = &user{Name: "jiaduo"}
func main() {
    //1.多goroutine并发调用获取实例
    for i := 0; i < 100; i++ {
        go onceCas(getInstance)
    // 2如果flag为1则打印u实例
    if flag == 1 {
        fmt.Printf("user,%+v", u)
}

如上代码声明了一个全局的user变量u,getInstance函数用来创建一个实例,代码1则多goroutine并发调用onceCase函数,意图只调用getInstance一次,保证u的唯一性。 代码2则判断如果flag值为1,则打印u的内容,那么问题是,如果flag确实为1,那么打印的u的内容一定是user,{Name:jiaduo}?

其实不然,因为如下onceCas函数的CAS操作只可以保证函数f只执行一次,但是不能保证CAS成功后也就是flag=1时,函数f已经被执行完毕了。

func onceCas(f func()) {
    if atomic.CompareAndSwapInt32(&flag, 0, 1) {
}

所以onceCas并不适合上面的场景。

那么假如我们不根据flag进行判断,而是根据u !=nil才打印那?

var u *user = nil
func getInstance() {
    u = &user{Name: "jiaduo"}
func main() {
    // 多goroutine并发调用获取实例
    for i := 0; i < 100; i++ {
        go onceCas(getInstance)
    if u != nil {
        fmt.Printf("user,%+v", u)
}

如上代码,我们可以保证 u != nil时,一定可以打印出user,{Name:jiaduo}?

答案其实也是否定的,这是因为函数getInstance中的

u = &user{Name: "jiaduo"}

操作不具有原子性,其执行可以分为三步:创建user对象分配内存,初始化user对象,然后把指针变量赋值为变量u。

由于指令重排序问题的存在,其执行顺序可能变成了:创建user对象分配内存,把指针变量赋值为变量u,初始化user对象。

所以即使我们判断 u!=nil但是打印出来的内容,可能还是u没初始化前的内容。

三、sync.once

3.1 sync.once 使用

go sdk内提供了sync.once函数可以帮我们解上面的问题,其可以保证传入sync.once的函数只执行一次,并且可以保证传入的函数执行期间,其他调用该once函数的goroutine必须阻塞等到传入的函数执行完毕。

修改代码如下:

var once sync.Once
func main() {
    // 多goroutine并发调用获取实例
    for i := 0; i < 100; i++ {
        go once.Do(getInstance)
    if u != nil {
        fmt.Printf("user,%+v", u)
}

如上我们通过调用once.Do方法保证getInstance方法只会被执行一次,并且在getInstance执行期间其他调用once.Do(getInstance)的goroutine会被阻塞,直到getInstance执行完毕。

3.2 sync.once 原理

sync.once内部原理很简单,如下:

type Once struct {
    //done变量用来标识函数是否执行完毕
    done uint32
        //m用来保证函数执行期间,其他goroutine阻塞
    m    Mutex
}

下面我们主看Do方法:

func (o *Once) Do(f func()) {
    //1.判断函数是否执行过,done=0说明没执行过
    if atomic.LoadUint32(&o.done) == 0 {
        o.doSlow(f)
func (o *Once) doSlow(f func()) {
    //2.加锁
    o.m.Lock()
    //6.函数返回后,释放锁
    defer o.m.Unlock()
    //3判断是否执行过
    if o.done == 0 {