初入门径
sync.Once提供了保证某个操作只被执行一次的功能,其最常应用于单例模式之下,例如初始化系统配置、保持数据库唯一连接,以及并发访问只需要初始化一次的共享资源。
单例模式有懒汉模式和饿汉模式两种
饿汉模式 顾名思义就是比较饥饿,所以一上来(服务启动时)就初始化。
懒汉模式 顾名思义就是偷懒,在获取实例的时候在进行初始化,但懒汉模式会有并发问题:有可能多个 goruntine 同时获取 对象都是 nil ,然后都开始创建了实例,就不满足单例模式了。解决办法是加锁
sync.Once就是懒汉模式
package main import ( "fmt" "sync" ) func main() { var once sync.Once fun1 := func() { fmt.Println("一只老虎") } once.Do(fun1) fun2 := func() { fmt.Println("两只老虎") } once.Do(fun2) }
输出为:
一只老虎
并发调用 Do()
package main import ( "fmt" "sync" "time" ) func main() { var once sync.Once for i := 0; i < 5; i++ { go func(i int) { fun1 := func() { fmt.Printf("i:=%d\n", i) } once.Do(fun1) }(i) } // 为防止主goroutine直接运行完,什么都看不到 time.Sleep(50 * time.Millisecond) }
在上面这段代码里,开启了5个并发的 goroutine ,不管执行多少次, 始终只打印一次. 至于 i 是多少,就看先执行的是哪个 g 了。
Once 保证只有第一次调用 Do() 方法时, 传递的 f (无参数无返回值的函数) 才会执行,并且之后不管调用的参数是否改变了,也不管f执行时是否发生了panic,之后都不再执行。
这段是官方Demo:
package main import ( "fmt" "sync" ) 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
源码实现
其代码实现很简洁, 从头到尾加注释不超过 70 行. 对外暴露了一个唯一接口 Do(f func())
, 使用起来也很简单,可以多次调用,但是只有第一次调用Do方法时f参数才会执行,这里的f是一个无参数无返回值的函数。
点击查看 sync.Once源码:
go
复制代码
// Copyright 2009 The Go Authors. All rights reserved.// Use of this source code is governed by a BSD-style// license that can be found in the LICENSE file.package sync
import (
"sync/atomic")// Once is an object that will perform exactly one action.type Once struct {
// done indicates whether the action has been performed.// It is first in the struct because it is used in the hot path.// The hot path is inlined at every call site.// Placing done first allows more compact instructions on some architectures (amd64/x86),// and fewer instructions (to calculate offset) on other architectures. // done用来表示当前的操作是否已经被执行, 0表示还没有执行过, 1表示已经执行 // done属性放在结构体的第一位,是因为它在hot path中使用 // hot path在每个调用点会被内联 // 将done放在结构体首位,像amd64/386等架构上可以允许更多的压缩指令,且在其他架构上更少的指令去计算偏移量 done uint32 m Mutex}// Do calls the function f if and only if Do is being called for the// first time for this instance of Once. In other words, given// var once Once// if once.Do(f) is called multiple times, only the first call will invoke f,// even if f has a different value in each invocation. A new instance of// Once is required for each function to execute.//// Do is intended for initialization that must be run exactly once. Since f// is niladic, it may be necessary to use a function literal to capture the// arguments to a function to be invoked by Do:// config.once.Do(func() { config.init(filename) })//// Because no call to Do returns until the one call to f returns, if f causes// Do to be called, it will deadlock.//// If f panics, Do considers it to have returned; future calls of Do return// without calling f.//// Do的作用主要是针对初始化且有且只能执行一次的场景。因为Do直到f返回才返回,`// 所以如果f内再调用Do则会导致死锁// 如果f执行过程中panic了 那么Do任务f已经执行完毕 未来再次调用不会再执行ffunc(o *Once) Do(f func()) {
// Note: Here is an incorrect implementation of Do://// if atomic.CompareAndSwapUint32(&o.done, 0, 1) {// f()// }//// Do guarantees that when it returns, f has finished.// This implementation would not implement that guarantee:// given two simultaneous calls, the winner of the cas would// call f, and the second would return immediately, without// waiting for the first's call to f to complete.// This is why the slow path falls back to a mutex, and why// the atomic.StoreUint32 must be delayed until after f returns.if atomic.LoadUint32(&o.done) == 0 {// Outlined slow-path to allow inlining of the fast-path. // 原子获取 done 的值,判断 done 值是否为 0,如果为 0 就调用 doSlow 方法,进行二次检查。 // 可能会存在并发 进入slow-path o.doSlow(f) }}func(o *Once) doSlow(f func()) {
// 二次检查时,持有互斥锁,保证只有一个 goroutine 执行。 o.m.Lock()defer o.m.Unlock()if o.done == 0 { //二次判断f是否已经被执行 // 二次检查,如果 done 的值仍为 0,则认为是第一次执行,执行参数 f,并将 done 的值设置为 1。 //即使此时有多个 goroutine 同时进入了 doSlow 方法,因为双检查的机制,后续的 goroutine 会看到 o.done 的值为 1,也不会再次执行 fdefer atomic.StoreUint32(&o.done, 1) f() }}
Once 结构体中包含两个字段, 分别是 uint32 类型的 done 和 Mutex 类型的 m.
Once 实现了两个方法, 分别是 Do 和 doSlow。其中 doSlow 是一个非可导出方法,只能在内部被 Do 方法调用.
Done 方法先原子获取 done 的值,如果 done 的值为 0,则调用 doSlow 方法进行二次检查,二次检查时,持有互斥锁,保证只有一个 goroutine 执行操作.如果二次检查的结果为 0,则认为是第一次执行,程序执行函数类型的参数 f,然后将 done 的值设置为 1.
注意事项
package main import ( "fmt" "sync" ) func main() { panicDo() //nestedDo() nestedDo2() } func panicDo() { once := &sync.Once{} defer func() { if err := recover(); err != nil { once.Do(func() { fmt.Println("run in recover") }) } }() once.Do(func() { panic("panic i=0") }) } func nestedDo() { once := &sync.Once{} once.Do(func() { once.Do(func() { fmt.Println("test nestedDo") }) }) } func nestedDo2() { once1 := &sync.Once{} once2 := &sync.Once{} once1.Do(func() { once2.Do(func() { fmt.Println("test nestedDo") }) }) }
(1). sync.Once()方法中传入的函数发生了panic,重复传入还会执行吗?
执行panicDo方法,不会打印任何东西. sync.Once.Do 方法中传入的函数只会被执行一次,哪怕函数中发生了 panic;
(2). sync.Once()方法传入的函数中再次调用sync.Once()方法会有什么问题吗?
会发生死锁! 执行nestedDo方法,会报 fatal error: all goroutines are asleep - deadlock! 根据源码实现,可知在第二个do方法会一直等doshow()中锁的释放导致发生了死锁;
(3). 执行nestedDo2,会输出什么?
会打印出 test nestedDo. once1,once2是两个对象,互不影响. 所以sync.Once是使方法只执行一次对象的实现。
官方库或知名项目中的使用
net包 系统的网络配置 就是存放在一个变量里,用sync.Once
控制读写
当且仅当第一次读某个变量时,进行初始化(写操作)
变量被初始化过程中,所有读都被阻塞(读操作;当变量初始化完成后,读操作继续进行
变量仅初始化一次,初始化完成后驻留在内存里