go sync.Once
提示
本文代码基于 go1.17.13,src/sync/once.go
# 一、简述
- 保证某段代码在程序执行期间只执行一次;
- 常用于服务启动时的配置初始化操作;
# 二、基本原理
- 通过原子计数和互斥锁的方式,记录函数 f 执行的次数,当计数为 0 时,加锁、计数并执行函数 f,**即使函数 f panic 了也只会执行一次;
- Once 首次使用后不允许被复制;
# 三、基本用法
点击查看
package main
import (
"sync"
)
func initConfig() {
}
func main() {
var one sync.Once
one.Do(initConfig)
}
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
- 该函数只能作为参数传递给
Do
函数才能保证只会被执行一次,如果是在其他地方继续调用则无法保证; - 即使多次执行
one.Do(initConfig)
操作,该函数也只会被执行一次;
# 四、源码解读
# 1, Once
// Once 是一个对象,它将只执行一个操作
type Once struct {
// done 表示是否已执行操作。
// 因为它在热路径中使用,热路径在每个呼叫站点上都内联。
// 将 done 放在第一位允许在某些架构 (amd64386) 上更紧凑的指令,而在其他架构上更少的指令(用于计算偏移)。
done uint32
m Mutex
}
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
done
原子计数,记录函数 f 执行的次数,它在结构中排在第一位,整个结构体变量的地址也就是该字段的地址,无需地址偏移就可以获取该字段的值;m
互斥锁,在计数和执行 f 函数时被调用;
# 2, Do
点击查看
// Do 当且仅当 Do 是首次为 Once 实例调用 Do 时,Do 才会调用函数 f。
// 换句话说,给定 var once Once,如果 once.Do(f) 被多次调用,只有第一次调用才会调用 f,即使 f 在每次调用中都有不同的值。
// 每个函数都需要一个新的 Once 实例才能执行。
// Do 用于必须只运行一次的初始化。config.once.Do(func() { config.init(filename) })
// 因为在对 f 的一次调用返回之前,对 Do 的调用不会返回,所以如果 f 导致调用 Do,它将死锁。
// 如果 f panic,Do 认为它已经返回了,未来再调用 Do 时将直接用返回而不调用 f。
func (o *Once) Do(f func()) {
// 注意:这是 Do 的错误实现:
//
// if atomic.CompareAndSwapUint32(&o.done, 0, 1) {
// f()
// }
//
// Do 保证当它返回时,f 已经完成。
// 此实现不会实现该保证:给定两个同时调用,cas 的获胜者将调用 f,第二个将立即返回,而无需等待第一个调用完成,此时 f 还没有完成。
// 这就是为什么慢速路径回落到互斥锁的原因,互斥锁能让第二个阻塞等待,获得锁后发现已经执行完再立即返回,
// 以及为什么 atomic.StoreUint32 必须延迟到 f 返回之后,保证先执行 f 再执行原子操作,只要 f 执行了,无论是否 panic 都执行 原子操作。
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 panic 该 defer 也能执行成功,后续 Do 将不会再调用 f
f()
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
- 采用原子操作判断
done
是否为 0,不为 0 则说明已经执行过 f 直接返回; - 为 0 则说明还没执行 f,转入慢路径执行;
- 慢路径执行时,先加锁,再次判断
done
是否为 0,避免再加锁期间 f 被其他 goroutine 执行完成; - 采用 defer 函数使
done
原子加一,再执行 f 函数,即使函数 f panic 了,done
的加一操作也能执行成功; - 采用互斥锁而不是 cas 的原因是保证
Done
返回时,函数 f 一定是执行完成的;- 互斥锁能够让其他 goroutine 阻塞等待,阻塞结束后 f 一定是执行完成的;
- 其他 goroutine 通过 cas 获取不到值时,则是直接返回,此时 f 可能还没有执行完成;
编辑 (opens new window)
上次更新: 2024/08/12, 19:17:34