sync:二. 延迟初始化(once)

sync.Once 是 Go 标准库提供的使函数只执行一次的实现。作用与 init 函数类似,但有区别。在某些情况下预先初始化一个变量会增加函数的启动延迟,如果实际执行时可能用不上这个变量,那么初始化就是非必须的。sync.Once很好的解决了这个问题,Once可以在任意的位置调用,并且只会执行一次,并发场景下是线程安全的。

Do方法

Once只提供了一个 func (o *Once) Do(f func()) 方法。Do方法调用一个函数,这里有三种情况:

  1. 如果函数没有被调用则调用它。
  2. 如果函数已经被调用完成则不调用.
  3. 如果函数正在被调用(未完成),则等待函数调用完成再返回。
func main() {
	var once sync.Once
	onceBody := func(){
		fmt.Println("Only once")
	}
	done := make(chan bool)
	for i:=0; i<10; i++ {
		go func() {
			once.Do(onceBody)
			done <- true
		}()
	}

	for i:=0; i<10; i++ {
		<-done
	}
}
// 结果:
// Only once

Once源码

Once结构

// Once 是一个将执行一个动作的对象。
type Once struct {
	// done 指示操作是否已执行。它在结构中是第一个,因为它用于热路径。热路径在每个调用站点内联。
	// 首先放置允许在某些架构(amd64x86)上使用更紧凑的指令,而在其他架构上使用更少的指令(计算偏移量)。
	done uint32
	m    Mutex
}

Once结构由两个字段一个 uint32类型的done来标志操作是否执行。这个字段放在第一个的原因是可以使用偏移量的形式来获取。然后就是一个互斥锁。

Do实现

func (o *Once) Do(f func()) {
    // 注意:这是 Do 的错误实现: if atomic.CompareAndSwapUint32(&o.done, 0, 1) { f() } 
    // Do 保证当它返回时, f 已经完成。这个实现不会实现这个保证:
    // 给定两个同时调用,cas 的获胜者将调用 f,第二个将立即返回,
    // 而不等待第一个对 f 的调用完成。这就是为什么慢速路径回退到互斥体,
    // 以及为什么 atomic.StoreUint32 必须延迟到 f 返回之后。
	if atomic.LoadUint32(&o.done) == 0 {
		// 慢速路径以允许内联快速路径。
		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()
	}
}

// 快速路径是指做功较少的路径,而慢速路径是指做功较多的路径。

这里可以看到如果第二次调用函数时发现第一次调用还未调用结束,会在doSlow中等待解锁,这里用一个互斥锁避免第一次调用未结束,第二次调用就返回的情况。

posted @ 2022-03-13 14:24  EthanWell  阅读(204)  评论(0编辑  收藏  举报