概念
sync.Once是Go语言标准库中的一个同步原语,用于确保某个操作只执行一次。它在多线程环境中非常有用,尤其是在需要初始化共享资源或执行某些一次性任务时。
简单示例
当我们在web服务访问某个路由时,如果需要事先获取某些配置,往往会写一个loadConfig函数,获取一个cfg配置项。多次路由访问所需要获取的配置项通常是相同的,如果对于每次路由访问,都加载一次loadConfig函数,会导致产生一些不必要的开销。如果loadConfig涉及到读取文件、解析配置、网络请求时,有可能会额外增加的请求响应时间,降低服务的吞吐量。使用sync.Once包提供的Do函数,就可以只在第一次请求时调用loadConfig函数加载配置,之后的请求都复用第一次请求的配置,缩短响应时间。
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
33
34
35
36
37
38
39
40
41
42
|
package main
import (
"log"
"net/http"
"sync"
)
type Config struct {
APIKey string
LogLevel string
}
var (
config *Config
once sync.Once
)
func loadConfig() {
// 模拟从文件或环境变量加载配置
config = &Config{
APIKey: "secret-key",
LogLevel: "debug",
}
log.Println("Configuration loaded")
}
func GetConfig() *Config {
once.Do(loadConfig) // 仅第一次访问时会执行loadConfig函数
return config
}
func handler(w http.ResponseWriter, r *http.Request) {
cfg := GetConfig()
log.Printf("Request handled with API key: %s", cfg.APIKey)
w.Write([]byte("OK"))
}
func main() {
http.HandleFunc("/", handler)
log.Fatal(http.ListenAndServe(":8888", nil))
}
|
源码解读
源码文件:src/sync/once.go (go 1.23 版本)
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
|
package sync
import (
"sync/atomic"
)
type Once struct {
done atomic.Uint32 // 是否已执行标识位,0-未执行 1-已执行
m Mutex // 互斥锁,确保并发安全
}
func (o *Once) Do(f func()) {
// 第一次执行Do函数时,原子操作检查o.done==0,执行doSlow函数后,o.done==1
// 第二次及之后执行Do函数,原子操作检查o.done标识位为1,Do函数不执行任何功能,确保了f函数只在第一次被执行
if o.done.Load() == 0 {
o.doSlow(f) // 调用doSlow函数执行f方法。第一次执行时,同一时间可能有多个goroutine尝试同时执行doSlow函数
}
}
func (o *Once) doSlow(f func()) {
o.m.Lock() // 加锁保护,避免多个goroutine同时绕过之前的原子操作检查,并发修改o.done的值
defer o.m.Unlock()
// 二次检查o.done的值,同一时间并发执行doSlow函数的goroutine,在第一个goroutine将o.done置为1并解除互斥锁后,
// 剩下的goroutine识别到自身的o.done已经被设为1,无法绕过二次检查
if o.done.Load() == 0 {
defer o.done.Store(1) // 需要在f()函数执行完成之后,原子性地将o.done设为1
f() // 执行f方法,一定只有一个goroutine会调用这个方法
}
}
|
可以看到,once.go文件的代码非常精炼。仅定义了一个含2个非导出字段done和m的结构体Once,并提供了一个doSlow方法用于执行f函数。当我们调用Do方法时,程序经历了几个关键步骤:
- 判断done标志位是否等于0,如果是,说明f函数还没有被执行,执行doSlow方法
- mu互斥锁加锁,防止多个goroutine并发操作
- double-check done标志位是否等于0,如果是,说明f函数还没有被执行,执行f函数
- f函数执行完成之后,再将done标志位原子性设为1。使用原子操作是从内存可见性的角度出发,如果done使用uint32而不是atomic.Uint32,done修改可能不会立即被其它goroutine感知,解锁后仍有可能存在goroutine的done等于0,重复执行f函数
- mu互斥锁解锁。此时进入到doSlow函数的其它goroutine也感知到了o.done等于1,不会重复执行f函数了
总结
上文就是针对Go源码sync.Once原理和使用方式的讲解。在实际开发中,sync.Once的使用还是非常普遍的。掌握sync.Once的底层原理,有助于我们在今后的开发中更有把握地利用它永远只执行一次函数的特性,完成复杂的技术需求或者业务需求。
|