Golang并发利器sync.Once怎么使用

sync.Once 基本概念

什么是 sync.Once

sync.OnceGoDoDo

sync.Once 的应用场景

sync.Once 主要用于以下场景:

sync.Once

sync.Once 应用实例

单例模式

sync.Once
package main

import (
   "fmt"
   "sync"
)

type Singleton struct{}

var (
   instance *Singleton
   once     sync.Once
)

func GetInstance() *Singleton {
   once.Do(func() {
      instance = &Singleton{}
   })
   return instance
}

func main() {
   var wg sync.WaitGroup

   for i := 0; i < 5; i++ {
      wg.Add(1)
      go func() {
         defer wg.Done()
         s := GetInstance()
         fmt.Printf("Singleton instance address: %pn", s)
      }()
   }

   wg.Wait()
}
GetInstanceonce.Do()instanceGetInstanceinstance = &Singleton{}s

延迟初始化

sync.Once
package main

import (
   "fmt"
   "sync"
)

type Config struct {
   config map[string]string
}

var (
   config *Config
   once   sync.Once
)

func GetConfig() *Config {
   once.Do(func() {
      fmt.Println("init config...")
      config = &Config{
         config: map[string]string{
            "c1": "v1",
            "c2": "v2",
         },
      }
   })
   return config
}

func main() {
   // 第一次需要获取配置信息,初始化 config
   cfg := GetConfig()
   fmt.Println("c1: ", cfg.config["c1"])

   // 第二次需要,此时 config 已经被初始化过,无需再次初始化
   cfg2 := GetConfig()
   fmt.Println("c2: ", cfg2.config["c2"])
}
Configsync.OnceGetConfigConfigConfig

sync.Once 实现原理

type Once struct {
   // 表示是否执行了操作
   done uint32
   // 互斥锁,确保多个协程访问时,只能一个协程执行操作
   m    Mutex
}

func (o *Once) Do(f func()) {
   // 判断 done 的值,如果是 0,说明 f 还没有被执行过
   if atomic.LoadUint32(&o.done) == 0 {
      // 构建慢路径(slow-path),以允许对 Do 方法的快路径(fast-path)进行内联
      o.doSlow(f)
   }
}

func (o *Once) doSlow(f func()) {
   // 加锁
   o.m.Lock()
   defer o.m.Unlock()
   // 双重检查,避免 f 已被执行过
   if o.done == 0 {
      // 修改 done 的值
      defer atomic.StoreUint32(&o.done, 1)
      // 执行函数
      f()
   }
}
sync.Oncedonemudoneuint32m
sync.OnceDodoSlowDofatomic.LoadUint32donefdoSlow
doSlowmfdonedoneffatomic.StoreUint32done

为什么会封装一个 doSlow 方法

doSlowslow-pathDoDofast-pathinlined

为什么会有双重检查(double check)的写法

done
atomic.LoadUint32donedonedoSlowdonefdonef

通过双重检查,可以在大多数情况下避免锁竞争,提高性能。

加强的 sync.Once

sync.OnceDoerrorDosync.Once
package main

import (
   "sync"
   "sync/atomic"
)


type Once struct {
   done uint32
   m    sync.Mutex
}

func (o *Once) Do(f func() error) error {
   if atomic.LoadUint32(&o.done) == 0 {
      return o.doSlow(f)
   }
   return nil
}

func (o *Once) doSlow(f func() error) error {
   o.m.Lock()
   defer o.m.Unlock()
   var err error
   if o.done == 0 {
      err = f()
      // 只有没有 error 的时候,才修改 done 的值
      if err == nil {
         atomic.StoreUint32(&o.done, 1)
      }
   }
   return err
}
Oncesync.OnceDoerrorerrordoneerror

sync.Once 的注意事项

死锁

sync.OncemDoDomutex
func main() {
   once := sync.Once{}
   once.Do(func() {
      once.Do(func() {
         fmt.Println("init...")
      })
   })
}

初始化失败

Doferrorsync.Oncesync.Onceonce