-
接口详情
Once结构体
// Once is an object that will perform exactly one action.
//
// A Once must not be copied after first use.
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/386),
// and fewer instructions (to calculate offset) on other architectures.
done uint32
m Mutex
}
Once结构体对外暴露的唯一的方法Do(f func()),f为需要执行的函数。
// 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.
//
func (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.
o.doSlow(f)
}
}
结合前面的注释,有以下几点注意事项:
- 只有在当前的Once实例第一次调用Do方法时,才会真正执行f。哪怕在多次调用Do中间f的值有所变化,也只会被实际调用一次。
- Do针对的是只希望执行一次的初始化操作,由于f是没有参数的,如果需要传参,可以采用包装一层func的形式来实现:config.once.Do(func(){config.init(filename)})
- 如果f抛出了panic,则相当于f函数已经返回,后续再次调用Do也不会触发对f的调用。
- 在对f的调用返回之前,不会返回对Do的调用,所以如果f方法中又调用Do方法,将会死锁。一定不能做如下操作:
func main() {
var once sync.Once
once.Do(func() {
once.Do(func() {
fmt.Println("hello kenmawr.")
})
})
}
-
实战用法
sync.Once的关键要点在于执行一些只希望做一次的操作,比如某个资源的清理、全局变量的初始化和单例模式等。
-
- 初始化
这里区别于init( )函数的初始化,虽然两者效果相当,但init( )函数是在包被首次加载时执行,如果未被使用,不仅浪费内存,还延缓了程序启动的时间。而sync.Once可以在任何位置调用,且并发安全;可以在实际依赖某个变量时才去初始化,减少性能浪费。
我们来看 Golang 官方的 html 库中的一个例子,我们经常使用的转义字符串函数 func UnescapeString(s string) string
在进入函数的时候,首先就会依赖包里内置的 populateMapsOnce 实例(本质是一个 sync.Once) 来执行初始化 entity 的操作。这里的entity是一个包含上千键值对的 map,如果init()时就去初始化,会浪费内存。
var populateMapsOnce sync.Once
var entity map[string]rune
func populateMaps() {
entity = map[string]rune{
"AElig;": '\U000000C6',
"AMP;": '\U00000026',
"Aacute;": '\U000000C1',
"Abreve;": '\U00000102',
"Acirc;": '\U000000C2',
// 省略后续键值对
}
}
func UnescapeString(s string) string {
populateMapsOnce.Do(populateMaps)
i := strings.IndexByte(s, '&')
if i < 0 {
return s
}
// 省略后续的实现
...
}
-
- 单例模式
package main
import (
"fmt"
"sync"
)
type Singleton struct{}
var singleton *Singleton
var once sync.Once
func GetSingletonObj() *Singleton {
once.Do(func() {
fmt.Println("Create Obj")
singleton = new(Singleton)
})
return singleton
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
obj := GetSingletonObj()
fmt.Printf("%p\n", obj)
}()
}
wg.Wait()
}
/*--------- 输出 -----------
Create Obj
0x119f428
0x119f428
0x119f428
0x119f428
0x119f428
**/
-
- 关闭channel
如果某个channel已经关闭,再次关闭会导致panic。
type T int
type MyChannel struct {
c chan T
once sync.Once
}
func (m *MyChannel) SafeClose() {
// 保证只关闭一次channel
m.once.Do(func() {
close(m.c)
})
}