Skip to content

sync.Once 源码阅读 #27

@LLLeon

Description

@LLLeon

在群里看到有人讨论 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()
	}
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions