一、介绍
sync.Once是 Go 标准库提供的使函数只执行一次的实现,常应用于单例模式,例如初始化配置、保持数据库连接等。作用与 init 函数类似,但有区别。
- init函数是package包被首次加载的时候,初始化使用。
- sync.Once可以在任意位置,全局中执行一次。
sync.Once源码在 src/sync/once.go
我们用 go doc sync.once 来整体看一下
type Once struct {
}
func (o *Once) Do(f func())
可以看到只有一个 sync.Once 结构体,以及一个执行的方法 Once.Do().
二、使用
func TestOnce(t *testing.T) {
//1 申明一个 sync.Once变量
var once sync.Once
for i := 0; i < 10; i++ {
go func() {
//2 使用Do()执行首次仅只有一次操作
once.Do(func() {
fmt.Println("hi")
})
}()
}
time.Sleep(time.Second)
}
备注:需要注意sync.Once的变量范围,比如这个变量范围在 TestOnce()函数里面,所以在其他的地方,还是可以调用fmt.Println("hi")函数的。仅是在这个函数里,调用一次。
这里的仅执行一次是在 sync.Once 变量范围内,只执行一次。
三、源码阅读
1、如何设计一个变量对象只能让读一次?
我们只需要定义一个变量 比如done uint32类型 ,每次使用的时候,查看一下done的值,如果为0就就可以执行,执行过了,我们就把值变成1,这样就能保证只能执行一次了。
2、官方源码的sync.Once
type Once struct {
// done 表示该操作是否执行.
// 将done放在结构体的首位,是因为在结构体done是最热数据
// The hot path is inlined at every call site.
// 将done放在首位可以让指令更紧凑
// and fewer instructions (to calculate offset) on other architectures.
done uint32
m Mutex
}
加了Mutex锁,是为了在高并发情况下,能够真正的只执行一次。
3、自己实现一下Do函数
func (o *Once) Do(f func()) {
o.mu.Lock()
defer o.mu.Unlock()
if o.done == 0 {
f()
o.done = 1
}
}
我们这样写,写一个测试函数
func TestOnce(t *testing.T) {
//1 申明一个 sync.Once变量
var once Once
for i := 0; i < 10; i++ {
go func() {
//2 使用Do()执行首次仅只有一次操作
once.Do(func() {
fmt.Println("hi")
})
}()
}
time.Sleep(time.Second)
}
输出如下:
hi
只输出一次 "hi" 说明代码没有问题。
3.1、锁的位置优化
目前的锁是一进入函数,就加锁,能否优化有以下,在写入的时候再加锁?
func (o *Once) Do(f func()) {
if o.done == 0 {
o.mu.Lock() //把锁移动到这里
defer o.mu.Unlock() //把锁移动到这里
f()
o.done = 1
}
}
我们把锁移动到,写入的时候再加锁,此时试验一下。
func TestOnce(t *testing.T) {
//1 申明一个 sync.Once变量
var once Once
for i := 0; i < 10; i++ {
go func() {
//2 使用Do()执行首次仅只有一次操作
once.Do(func() {
fmt.Println("hi")
})
}()
}
time.Sleep(time.Second)
}
输出如下:
=== RUN TestOnce
hi
hi
hi
hi
hi
hi
hi
hi
hi
hi
--- PASS: TestOnce (1.00s)
发现在并发情况下有问题,原因在于,多个协程读取的时候,没有锁,此时都是0,都拿到然后自己去执行了。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。