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

推荐订阅源

C
CXSECURITY Database RSS Feed - CXSecurity.com
aimingoo的专栏
aimingoo的专栏
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
D
Docker
博客园 - 叶小钗
Recent Announcements
Recent Announcements
人人都是产品经理
人人都是产品经理
云风的 BLOG
云风的 BLOG
Vercel News
Vercel News
Hugging Face - Blog
Hugging Face - Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
美团技术团队
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
F
Fortinet All Blogs
博客园 - 三生石上(FineUI控件)
Microsoft Security Blog
Microsoft Security Blog
H
Heimdal Security Blog
有赞技术团队
有赞技术团队
The Cloudflare Blog
S
Secure Thoughts
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
T
The Blog of Author Tim Ferriss
M
MIT News - Artificial intelligence
Google DeepMind News
Google DeepMind News
Schneier on Security
Schneier on Security
N
News and Events Feed by Topic
小众软件
小众软件
C
Cybersecurity and Infrastructure Security Agency CISA
Application and Cybersecurity Blog
Application and Cybersecurity Blog
博客园 - Franky
T
Tenable Blog
The Last Watchdog
The Last Watchdog
腾讯CDC
量子位
Google DeepMind News
Google DeepMind News
Help Net Security
Help Net Security
雷峰网
雷峰网
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Cyberwarzone
Cyberwarzone
S
Securelist
Martin Fowler
Martin Fowler
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
罗磊的独立博客
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
P
Proofpoint News Feed
D
Darknet – Hacking Tools, Hacker News & Cyber Security
I
InfoQ
Spread Privacy
Spread Privacy
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
H
Hackread – Cybersecurity News, Data Breaches, AI and More

蛮荆

如何获取更多的免费服务器 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 内存模型 Kubernetes 核心概念 软件版本语义化
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