惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

SecWiki News
SecWiki News
I
InfoQ
The Cloudflare Blog
人人都是产品经理
人人都是产品经理
博客园 - Franky
T
Tailwind CSS Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
量子位
博客园_首页
罗磊的独立博客
V
V2EX
李成银的技术随笔
大猫的无限游戏
大猫的无限游戏
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
True Tiger Recordings
Vercel News
Vercel News
Cyberwarzone
Cyberwarzone
Cisco Talos Blog
Cisco Talos Blog
F
Fox-IT International blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
M
Microsoft Research Blog - Microsoft Research
Know Your Adversary
Know Your Adversary
爱范儿
爱范儿
The Register - Security
The Register - Security
G
Google Developers Blog
The Hacker News
The Hacker News
Malwarebytes
Malwarebytes
S
Securelist
博客园 - 三生石上(FineUI控件)
Jina AI
Jina AI
T
Threat Research - Cisco Blogs
T
The Exploit Database - CXSecurity.com
S
SegmentFault 最新的问题
博客园 - 叶小钗
F
Fortinet All Blogs
Apple Machine Learning Research
Apple Machine Learning Research
宝玉的分享
宝玉的分享
博客园 - 聂微东
T
Threatpost
博客园 - 【当耐特】
D
Docker
P
Privacy & Cybersecurity Law Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
G
GRAHAM CLULEY
V
Visual Studio Blog
C
Cisco Blogs
IT之家
IT之家
S
Security Archives - TechRepublic
Latest news
Latest news
阮一峰的网络日志
阮一峰的网络日志

蛮荆

如何获取更多的免费服务器 Kubernetes 调度器队列 - 设计与实现 Kubernetes 调度器 - 核心流程 Kubernetes Networking Model & CNI Kubernetes 控制器管理总结 Kubernetes CronJob 设计与实现 Kubernetes Job 设计与实现 Kubernetes HPA 设计与实现 Kubernetes Deployment 滚动更新实现原理 Kubernetes GC 设计与实现 Kubernetes Pod 驱逐 - 设计与实现 Kubernetes Daemonset 设计与实现 Kubernetes ReplicaSet 设计与实现 Kubernetes EndPoint 设计与实现 Kubernetes Informer 设计与实现 降本增效之应用优化 (三) 日志存储与检索 Kubernetes Pod 设计与实现 - 创建流程 Kubernetes 探针设计与实现 Unix 编程艺术名句摘录 Kubernetes - CRI 概述 Golang 编译速度为什么这么快? Kubernetes Pod 设计与实现 - Pause 容器 Kubernetes - kube-proxy 代理模式工程优化 Kubernetes 应用最佳实践 - 优雅关闭长连接 Kubernetes Service 类型和会话亲和性 Kubernetes 为什么需要 Ingress Kubernetes 架构 - 控制平面和数据平面 降本增效之应用优化 (二) 大报表 Go 语言如何获取 CPU 利用率 降本增效之应用优化 (一) Redis 业务规则引擎演变过程简述 微服务中的熔断算法 漏桶算法和令牌桶算法 jsonparser 为什么比标准库的 encoding/json 快 10 倍 ? zap 高性能设计与实现 HTTP Router 算法演进 布谷鸟过滤器 fastcache 高性能设计与实现 Web 常见的三个安全问题 ants Code Reading 布谷鸟过滤器 Go 线程安全 map 方案选型 布隆过滤器 死锁、活锁、饥饿、自旋锁 sync.Pool Code Reading Go 内存管理概述 Go netpoll Code Reading goroutine 泄漏与检测 time/Timer Code Reading GMP Scheduler Code Reading Go channel 的 15 条规则和底层实现 为什么 Linux “一切皆文件” context.Context Code Reading runtime/HACKING.md Goland 最佳实践 互联网开发与金庸武学 为什么 Redis 6.0 引入多线程模型? Kubernetes 应用最佳实践 - 金丝雀发布 容器中如何正确配置 GOMAXPROCS ? singleflight Code Reading sync.Map Code Reading sync.Cond Code Reading sync.WaitGroup Code Reading sync.RWMutex Code Reading sync.Mutex Code Reading Go 无锁编程 sync/atomic Code Reading goroutine 交替打印奇偶数 GODEBUG Go 并发模式 Go 汇编 UUID 通用技术选型 Kubernetes 应用最佳实践 - 水平自动伸缩 Go 高性能 Tips fasthttp 为什么比标准库 net/http 快 10 倍 ? 技术文章配图指南 ChatGPT 初体验 Docker 网络原理概览 iptables 的五表五链 Kubernetes 应用最佳实践 - 亲和性和污点容忍度 Go 的反射与三大定律 Docker 官方提供的最佳实践 Go 语言内置的设计模式 HTTP1 到 HTTP3 的工程优化 Kubernetes 应用最佳实践 - Sidecar 模式 Kubernetes 应用最佳实践 - init 容器和钩子函数 为什么 recover 必须在 defer 中调用? 为什么 defer 的执行顺序和注册顺序不同? Go map 设计与实现 Go 切片扩容底层实现 Go 语言中的零拷贝 Go Delve 云原生和边缘计算简介 Kubernetes Pod 服务质量等级 Kubernetes 应用最佳实践 - 探针 Kubernetes 应用最佳实践 - 资源请求和限制 CDN 原理 Kubernetes 应用最佳实践 - 开篇 缓存策略和模式 Go 内存模型
sync.Once Code Reading
2023-04-23 · via 蛮荆

2023-04-23 Golang 并发编程 Go 源码分析 读代码

概述

sync.Once 可以保证某段程序在运行期间的只执行一次,典型的使用场景有初始化配置, 数据库连接等。

sync.Once 流程图

与 init 函数差异

  • init 函数是当所在的 package 首次被加载时执行,若迟迟未被使用,则既浪费了内存,又延长了程序加载时间
  • sync.Once 方法可以在代码的任意位置初始化和调用,并发场景下是线程安全的,因此可以延迟到使用时再调用 (懒加载)

示例

通过一个小例子展示 sync.Once 的使用方法。

package main

import (
	"fmt"
	"sync"
)

type Config struct {
	Server string
	Port   int
}

var (
	once   sync.Once
	config *Config
)

func InitConfig() *Config {
	once.Do(func() {
		fmt.Println("mock init ...") // 模拟初始化代码
	})

	return config
}

func main() {
	// 连续调用 5 次初始化方法
	for i := 0; i < 5; i++ {
		_ = InitConfig()
	}
}
$ go run main.go

# 输出如下
mock init ...

从输出的结果中可以看到,虽然我们调用了 5 次初始化配置方法,但是真正的初始化方法只执行了 1 次,实现了设计模式中 单例模式 的效果。

方法调用结果

内部实现

接下来,我们来探究一下 sync.Once 的内部实现,文件路径为 $GOROOT/src/sync/once.go,笔者的 Go 版本为 go1.19 linux/amd64

Once 结构体

package sync

import (
	"sync/atomic"
)

// Once 是一个只执行一次操作的对象
// Once 一旦使用后,便不能再复制
//
// 在 Go 内存模型术语中,once.Do(f) 中函数 f 的返回值会在 once.Do() 函数返回前完成同步
type Once struct {
	done uint32
	m    Mutex
}

sync.Once 的结构体有 2 个字段,m 表示持有一个互斥锁,这是并发调用场景下 只执行一次 的保证, done 字段表示调用是否已完成,使用的字段类型是 uint32, 这样就可以使用标准库中 atomic 包里面 *Uint32 系列方法了,

为什么没有使用 bool 类型呢? 因为标准库中 atomic 包并未提供针对 bool 类型的相关方法,如果适用 bool 类型,操作时就需要转换为 指针 类型, 然后使用 atomic.*Pointer 系列方法操作,这样会造成内存占用过多 (bool 占用 1 个字节,指针 占用 8 个字节) 和性能损耗 (参数类型转换)。

done 字段

sync.Once 结构体

done 作为结构体的第一个字段,能够减少 CPU 指令,也就是能够提升性能,具体来说:

热路径 hot path 是程序非常频繁执行的一系列指令,sync.Once 绝大部分场景都会访问 done 字段,所以 done 字段是处于 hot path 上的,这样一来 hot path 编译后的机器码指令更少,性能更高。

为什么放在第一个字段就能够减少指令呢?因为结构体第一个字段的地址和结构体的指针是相同的,如果是第一个字段,直接对结构体的指针解引用即可。 如果是其他的字段,除了结构体指针外,还需要计算与第一个值的 偏移量。在机器码中,偏移量是随指令传递的附加值,CPU 需要做一次偏移值与指针的加法运算, 才能获取要访问的值的地址,因此访问第一个字段的机器码更紧凑,速度更快。

Do 方法

// 当且仅当第一次调用实例 Once 的 Do 方法时,Do 去调用函数 f
// 换句话说,调用 once.Do(f) 多次时,只有第一次调用会调用函数 f,即使 f 函数在每次调用中有不同的参数值

// 并发调用 Do 函数时,需要等到其中的一个函数 f 执行之后才会返回
// 所以函数 f 中不能调用同一个 once 实例的 Do 函数 (递归调用),否则会发生死锁
// 如果函数 f 内部 panic, Do 函数同样认为其已经返回,将来再次调用 Do 函数时,将不再执行函数 f
// 所以这就要求我们写出健壮的 f 函数
func (o *Once) Do(f func()) {
    // 下面是一个错误的实现
    // if atomic.CompareAndSwapUint32(&o.done, 0, 1) {
    //   f()
    // } 
	
    // 错误原因分析: 
    // 这里以数据库连接场景为例,在并发调用情况下,假设其中 1 个 goroutine 正在执行函数 f (初始化连接),
	// 此时其他的 goroutine 将不会等待这个 goroutine 执行完成,而是会直接返回,
    // 如果连接发生了一些延迟,导致函数 f 还未执行完成,那么此时连接其实还未建立,
	// 但是其他的 goroutine 认为函数 f 已经执行完成,连接已建立,可以开始使用了
	// 最后当其他 goroutine 使用未建立的连接操作时,产生报错

	// 要解决上面的问题, 就需要确保当前函数返回时, 函数 f 已经执行完成,
	// 这就是 slow path 退回到互斥锁的原因,以及为什么 atomic.StoreUint32 需要延迟到函数 f 返回之后
	if atomic.LoadUint32(&o.done) == 0 {
		o.doSlow(f) // slow-path 允许内联
	}
}

错误实现示例

doSlow 方法

func (o *Once) doSlow(f func()) {
	// 并发场景下,可能会有多个 goroutine 执行到这里
    o.m.Lock()  // 但是只有 1 个 goroutine 能获取到互斥锁
    defer o.m.Unlock()
	
	// 注意下面临界区内的判断和修改
	
    // 在 atomic.LoadUint32 时为 0 ,不等于获取到锁之后也是 0,所以需要二次检测
	// 因为已经获取到互斥锁,根据 Go 的同步原语约束,对于字段 done 的修改需要在获取到互斥锁之前同步
	// 所以这里直接访问字段即可,不需要调用 atomic.LoadUint32 方法
	// 如果有其他 goroutine 已经修改了字段 done,那么就不会进入条件分支,没有任何影响 
	if o.done == 0 {
		// 只要函数 f 成功执行过一次,就将 o.done 修改为 1
		// 这样其他 goroutine 就不会再执行了,从而保证了函数 f() 只会执行一次,
		// 这里必须使用 atomic.StoreUint32 方法来满足 Go 的同步原语约束
        defer atomic.StoreUint32(&o.done, 1)
        f()
    }
}

正确实现示例

小结

sync.Once 的源代码只有短短十几行,看似简单的条件分支背后充斥着 并发执行, 原子操作, 同步原语 等基础原理, 深入理解这些原理之后,可以帮助我们更好地构建并发系统,解决并发编程中遇到的问题。

Reference