news 2026/6/10 14:06:34

恭喜你发明了 Golang 的 sync.Once

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
恭喜你发明了 Golang 的 sync.Once

现在有一个命题作文,需要一个结构体,该结构体具有一个方法,方法的传参是一个函数,比如数据库客户端的初始化,需要保证无论如何或者多次调用该方法,传入的 函数只会执行一次,即数据库客户端只初始化一次,问该如何设计这个结构体。

方案一 If-Else

首先想到的就是在结构体内记录一下是否执行过传参的函数,如果没有执行过就执行并且记录下来,如果执行过就不再执行,如此看来只是 if-else 而已,写起来也非常顺畅,代码如下。

type Once struct {

done bool

}

func (o *Once) Do(f func()) {

if !o.done {

o.done = true

f()

}

}

但是这样做有一个问题,在多协程并发的时候无法保证只执行一次。为何,请听我细细讲解,本质是因为对于 done 的操作不是原子操作,多个协程执行顺序不确定,若一个协程判定 !done 成立但尚未执行 f(),此时如果有其他协程也判定了 !done 成立,就会重复执行 f(),流程图如下。

f()

Once实例

协程B

协程A

f()

Once实例

协程B

协程A

初始状态: done = false

并发读取 done 字段

f() 被执行了两次!

Do(f)

Do(f)

检查 !done (true)

检查 !done (true)

执行 f()

执行 f()

done = true

done = true

方案二 CAS

既然问题根源是 done 的判断和修改两个操作无法保证原子性,那自然就会想到使用 Go 源码中的原子操作 CompareAndSwap,后面简称为 CAS,通过使用硬件的功能达成判断和修改两个操作的原子性。CAS 在 Go 中的应用十分广泛,比如 Go 源码中的 sync.Mutex 本质就是 for 循环 + CAS。

type Once struct {

done atomic.Uint32

}

func (o *Once) Do(f func()) {

if o.done.CompareAndSwap(0, 1) {

f()

}

}

上面的问题算是告一段落了,但是还有另外一个问题,因为在我们的需求中 f 函数是一个前置操作,通常情况下后面会紧接着执行其他操作,比如访问数据库。同样是并发场景下,一个协程 CAS 成功开始执行 f 但尚未执行完,另外一个协程 CAS 失败就直接进行下一步,而客户端这个时候还没初始化完成还没准备好,就会访问数据库失败,流程图如下。

数据库操作

f()初始化

Once实例(CAS)

协程B

协程A

数据库操作

f()初始化

Once实例(CAS)

协程B

协程A

CAS成功 获得执行权

CAS失败 直接返回

初始化数据库客户端ing

客户端尚未初始化完成

Do(f) - CAS(0,1)

Do(f) - CAS(0,1)

开始执行f()

访问数据库

f()执行完成

正常访问数据库

方案三 Mutex

其实就是需要在上面的基础上增加一个等待机制,那说到阻塞或者等待资源释放,第一个想到的就是 Go 源码中的 sync.Mutex,互斥锁的作用是同一时间只能有一个协程持有锁去判断和操作 done,其他协程拿不到锁都会阻塞,于是第一个拿到锁的协程会执行初始化,后面并发过来的协程就乖巧地静静等待。

type Once struct {

done bool

m sync.Mutex

}

func (o *Once) Do(f func()) {

o.m.Lock()

defer o.m.Unlock()

if o.done == false {

f()

o.done = true

}

}

这样一来完全足以满足我们的需求,流程图如下。

数据库

f()初始化

Once实例(带Mutex)

协程B

协程A

数据库

f()初始化

Once实例(带Mutex)

协程B

协程A

Do(f) - 获取锁

Do(f) - 尝试获取锁

检查 done == false (true)

执行f()初始化

done = true

释放锁

成功获得锁

检查 done == true

释放锁

访问数据库

访问数据库

方案四 Mutex + Atomic

一旦初始化之后就不需要再初始化了,而之后每次执行 Do 时还都去获取锁实在太浪费了,要知道高并发下获取锁的代价是很高的。自然而然就会想到 CAS 的操作消耗很少,那在获取锁的流程之前先 CAS 一下不就好了,虽然在初始化前是多了一步操作,但是毕竟只要初始化完成之后就会只走 CAS 的逻辑了,所以对于长时间持续运行的程序来熟还是更优的。

type Once struct {

done atomic.Uint32

m sync.Mutex

}

func (o *Once) Do(f func()) {

if o.done.Load() == 0 {

o.doSlow(f)

}

}

func (o *Once) doSlow(f func()) {

o.m.Lock()

defer o.m.Unlock()

if o.done.Load() == 0 {

defer o.done.Store(1)

f()

}

}

这里也有一些小技巧

持有互斥锁的期间不用担心并发问题,所以不再用 CAS 了,但是需要提前判断 done,所以依旧用了原子操作的 Load 和 Store。

使用 defer 执行 Store(1) 是有些思考在里面的,试想一下如果 f 发生了 panic,如果不加 defer 那么 done 依旧是 0,后面仍会重复执行 f,这就违背了 f 只执行一次的初衷。

而且 defer 的执行顺序是栈的顺序,所以 Store(1) 先执行,再释放锁,这也是对的。

至此,恭喜你发明了 sync.Once!

Sugar

为了方便开发时快速使用 sync.Once, Go 源码中还有下面有两个工具函数。

OnceFunc

基础款式需要保存 Once 结构体和 f 函数,而宝宝款式 OnceFunc 将两者整合成了一个闭包函数,可以作为全局变量直接调用,可以小幅度减少代码量,同时逻辑内敛减少阅读代码时的心智负担。只不过内部 once,valid,p 三个变量都内存逃逸了,如果追求性能的话还是用基础款式比较好。

func OnceFunc(f func()) func() {

var (

once Once

valid bool

p any

)

// Construct the inner closure just once to reduce costs on the fast path.

g := func() {

defer func() {

p = recover()

if !valid {

// Re-panic immediately so on the first call the user gets a

// complete stack trace into f.

panic(p)

}

}()

f()

f = nil // Do not keep f alive after invoking it.

valid = true // Set only if f does not panic.

}

return func() {

once.Do(g)

if !valid {

panic(p)

}

}

}

OnceValue

OnceValue 则是在 OnceFunc 的基础上增加了一个返回值,感兴趣的可以去看下源码,这里就不多做介绍了

// OnceValue returns a function that invokes f only once and returns the value

// returned by f. The returned function may be called concurrently.

//

// If f panics, the returned function will panic with the same value on every call.

func OnceValue[T any](f func() T) func() T

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/9 20:20:30

如何快速创建专业神经网络图:NN-SVG完全使用手册

如何快速创建专业神经网络图:NN-SVG完全使用手册 【免费下载链接】NN-SVG NN-SVG: 是一个工具,用于创建神经网络架构的图形表示,可以参数化地生成图形,并将其导出为SVG文件。 项目地址: https://gitcode.com/gh_mirrors/nn/NN-S…

作者头像 李华
网站建设 2026/6/9 12:16:28

3.5%成本颠覆千亿模型格局:Cogito v2 70B混合推理技术革新

3.5%成本颠覆千亿模型格局:Cogito v2 70B混合推理技术革新 【免费下载链接】cogito-v2-preview-llama-70B 项目地址: https://ai.gitcode.com/hf_mirrors/unsloth/cogito-v2-preview-llama-70B 导语 当AI不再"过度依赖计算资源"——旧金山AI初创…

作者头像 李华
网站建设 2026/6/10 13:45:59

电商系统MySQL分表实战:订单数据拆分方案

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容: 设计一个电商订单分表系统,基于订单创建时间按月拆分数据。需要生成:1)分表创建的SQL脚本 2)数据迁移的存储过程 3)跨分表查询的视图。要求处理1000万订单数…

作者头像 李华
网站建设 2026/6/10 13:42:55

设计转换工具的终极指南:如何实现从设计到动画的无缝工作流

设计转换工具的终极指南:如何实现从设计到动画的无缝工作流 【免费下载链接】AEUX Editable After Effects layers from Sketch artboards 项目地址: https://gitcode.com/gh_mirrors/ae/AEUX 在当今快节奏的创意产业中,设计转换工具已成为提升工…

作者头像 李华
网站建设 2026/5/31 15:21:19

Maputnik 开源地图样式编辑器快速入门指南

Maputnik 开源地图样式编辑器快速入门指南 【免费下载链接】maputnik An open source visual editor for the MapLibre Style Specification 项目地址: https://gitcode.com/gh_mirrors/ma/maputnik Maputnik 是一个针对 MapLibre GL 样式规范的开源视觉编辑器&#xff…

作者头像 李华
网站建设 2026/6/10 13:57:55

CMATH终极指南:如何用AI模型通过小学数学考试?[特殊字符]

CMATH终极指南:如何用AI模型通过小学数学考试?🚀 【免费下载链接】cmath CMATH: Can your language model pass Chinese elementary school math test? 项目地址: https://gitcode.com/gh_mirrors/cm/cmath 想要知道当前最火的大语言…

作者头像 李华