LLLeon / Blog

LLLeon 的部落格
15 stars 4 forks source link

sync.Once 源码阅读 #27

Open LLLeon opened 3 years ago

LLLeon commented 3 years ago

在群里看到有人讨论 Once 的实现细节,也看了下源码,记录一下。

Once 的一般使用场景:确保初始化操作只执行一次。

直接看源码吧:

package sync

import (
    "sync/atomic"
)

type Once struct {
    done uint32
    m    Mutex
}

func (o *Once) Do(f func()) {
    // 下面是一个 Do 的错误实现:
    //
    //  if atomic.CompareAndSwapUint32(&o.done, 0, 1) {
    //      f()
    //  }
    //
    // Do 要确保它返回时,f 已经执行完毕。
  // 而这个实现不能确保这一点。考虑这个情况:
  //   1. 当有多个 goroutine 来执行 Do 时,goroutine1 判断 o.done 为 0,
  // 将其修改为 1 后开始执行 f。
  //   2. goroutine2 判断 o.done 为 1,直接返回,此时 f 中的资源不一定初始化完毕。
  //   3. 如果此时 goroutine2 就开始使用 f 未初始化完毕的资源,可能会出现意外情况。
    if atomic.LoadUint32(&o.done) == 0 {
        o.doSlow(f)
    }
}

// 接上面,这就是为什么 doSlow 里面使用了 mutex。考虑这个情况:
//   1. goroutine1 原子加载 o.done,此时值为 0,开始执行 doSlow。goroutine1 先获取到锁,
// 执行 f 但未返回。
//   2. goroutine2 此时也原子加载 o.done,由于 goroutine1 还未修改其值,所以还为 0。
// 也执行 doSlow,但获取不到锁,阻塞在这里。
//   3. goroutine1 执行完毕 f,返回前将 o.done 设为 1 并释放锁。
//   4. goroutine 2 获取到锁,判断 o.done 为 1,直接返回。此时 f 中的资源已初始化完毕,可以使用。
func (o *Once) doSlow(f func()) {
    o.m.Lock()
    defer o.m.Unlock()
    if o.done == 0 {
    // 由于要确保 f 执行完毕后(不管成功或失败)再修改 o.done 的值,所以使用 defer。
    // 后续对 Do 的调用,都不会执行 f。
        defer atomic.StoreUint32(&o.done, 1)
        f()
    }
}