Skip to content

once

sync.Once 是 Go 标准库中的一个同步工具,用于确保某个函数在并发环境下只执行一次。它通常用于延迟初始化、全局资源的初始化等场景,确保某个特定的操作只会被执行一次,即使有多个协程并发执行。

go
package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup
	var once sync.Once
	for i := range 10 {
		wg.Add(1)
		go func() {
			defer wg.Done()
			once.Do(func() {
				fmt.Println(i)
			})
		}()
	}
	wg.Wait()
}

在上面的代码中,总共有 10 个协程,但无论如何,只有一个协程会执行 once.Do() 中的 fmt.Println(i)。具体是哪一个协程会打印,取决于哪个协程最先到达 once.Do()

结构

go
type Once struct {
    done atomic.Uint32
    m    Mutex
}

它的内部结构非常简单

  1. done,一个原子值,用于表示是否被执行过
  2. m,互斥锁,用于阻塞其它想要执行的协程

它的原理就是在执行前先加锁,然后更新done,执行完毕后解锁。

Do

go
func (o *Once) Do(f func()) {
	if o.done.Load() == 0 {
		o.doSlow(f)
	}
}

func (o *Once) doSlow(f func()) {
	o.m.Lock()
	defer o.m.Unlock()
	if o.done.Load() == 0 {
		defer o.done.Store(1)
		f()
	}
}

代码整体上非常简单,流程如下:

  1. 直接加载原子值,如果已经执行过了就直接返回
  2. 没有执行过,则尝试持有锁,这里可能是多个协程在竞争互斥锁
  3. 当胜者成功持有锁后,还需要再一次判断done的值,因为在你持有锁后,别人可能已经完成加锁-执行-解锁一条龙了,这时你才刚被唤醒。
  4. 执行目标函数
  5. 更新done
  6. 释放锁

小结

sync.Once 是一个非常简洁而高效的同步工具,它确保了并发环境下某个操作只执行一次,从而避免了重复工作或资源的浪费。

Golang学习网由www.golangdev.cn整理维护