【Go网络编程】Once源码阅读

51 阅读2分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 21 天,点击查看活动详情

Once

Once类型可以确保函数只被调用一次。Once类型有一个Do方法,Do方法接收一个无参函数作为参数,并且确保该函数只被执行一次,无论在任何时候被调用。在第一次调用Do方法时,会执行这个函数,以后的调用会直接返回,不会再次执行函数。Once类型可以用于一些只需要进行一次初始化的操作,比如单例模式等。

以下是一个使用Once类型的例子:

var once sync.Once

func main() {
    // Do some initialization work, but only once.
    once.Do(func() {
        fmt.Println("Initializing...")
    })

    // Now, this line will not print anything, because the initialization work is already done.
    once.Do(func() {
        fmt.Println("This should not print.")
    })
}

Once的错误实现

一个简单的想法是用一个变量标记函数是否执行过,再利用原子操作修改这个变量就行,如果执行过直接返回:


type Once struct {
    done uint32
}

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

但是这是官方指出的错误实现方法,因为当函数执行很慢时,如果并发调用,例如初始化资源,会得到空值。Do应该保证当它返回时,f已经完成。

源码解析

一个正确的 Once 实现要使用一个互斥锁,这样初始化的时候如果有并发的 goroutine,就会进入doSlow 方法。

type Once struct {
   done uint32
   m    Mutex
}

func (o *Once) Do(f func()) {
   if atomic.LoadUint32(&o.done) == 0 {
      // inline 加速
      o.doSlow(f)
   }
}

func (o *Once) doSlow(f func()) {
   o.m.Lock()
   defer o.m.Unlock()

   // 双检查
   if o.done == 0 {
      defer atomic.StoreUint32(&o.done, 1)
      f()
   }
}
  • done:是否执行过的标志
  • m:互斥锁,辅助确保执行一次

Do:

  • 执行过就直接退出
  • 没执行过就走doSlow

doSlow:

  • 先加锁,双检查,执行f,原子修改标志,解锁

双检查的作用:

  • 当函数执行时,Do会进入doSlow并阻塞,当函数执行完,解锁后,这些阻塞的doSlow还是会进入,要再检查一下是不是执行过,没有执行过才再执行。当然标准改完以后再执行的Do就不会进入doSlow了,double check主要是应对f执行时的do操作。