恭喜你发明了 Golang 的 sync.Once

简介: 本文探讨如何设计一个保证函数只执行一次的Go结构体,对比if-else、CAS、Mutex及Mutex+Atomic四种方案,逐步优化至并发安全且高性能的实现,最终揭示sync.Once的设计原理,并介绍OnceFunc与OnceValue的便捷用法。

欢迎访问我的个人小站 莹的网络日志 ,不定时更新文章和技术博客~

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

方案一 If-Else

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

type Once struct {
   
    done bool
}

func (o *Once) Do(f func()) {
   
    if !done {
   
        done = true
        f()
    }
}

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

screenshot-20251011-193537.png

方案二 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 失败就直接进行下一步,而客户端这个时候还没初始化完成还没准备好,就会访问数据库失败,流程图如下。

screenshot-20251011-193551.png

方案三 Mutex

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

type Once struct {
   
    done bool
    m    Mutex
}

func (o *Once) Do(f func()) {
   
    o.m.Lock()
    defer o.m.Unlock()
    if o.done == false {
   
       f()
       o.done = true
    }
}

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

screenshot-20251011-193601.png

方案四 Mutex + Atomic

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

type Once struct {
   
    done atomic.Uint32
    m    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

欢迎访问我的个人小站 莹的网络日志 ,不定时更新文章和技术博客~

相关文章
|
20天前
|
算法
基于MATLAB/Simulink平台搭建同步电机、异步电机和双馈风机仿真模型
基于MATLAB/Simulink平台搭建同步电机、异步电机和双馈风机仿真模型
|
21天前
|
人工智能 安全 API
近期 AI 领域的新发布所带来的启示
2024 年以来,AI 基础设施的快速发展过程中,PaaS 层的 AI 网关是变化最明显的基建之一。从传统网关的静态规则和简单路由开始,网关的作用被不断拉伸。用户通过使用网关来实现多模型的流量调度、智能路由、Agent 和 MCP 服务管理、AI 治理等,试图让系统更灵活、更可控、更可用。国庆期间 AI 界发布/升级了一些产品,我们在此做一个简报,从中窥探下对 AI 网关演进新方向的启示。
241 27
|
20天前
|
关系型数据库 MySQL Linux
Centos 7.2 系统安装mysql5.7.10指定版本
本文介绍在CentOS 7.2系统上安装MySQL 5.7.10的完整步骤,包括下载RPM包、解压、依赖处理、强制安装、服务启动与状态检查,并通过日志获取临时密码后修改为自定义密码,确保MySQL服务正常运行。
192 9
|
21天前
|
人工智能 安全 Java
分布式 Multi Agent 安全高可用探索与实践
在人工智能加速发展的今天,AI Agent 正在成为推动“人工智能+”战略落地的核心引擎。无论是技术趋势还是政策导向,都预示着一场深刻的变革正在发生。如果你也在探索 Agent 的应用场景,欢迎关注 AgentScope 项目,或尝试使用阿里云 MSE + Higress + Nacos 构建属于你的 AI 原生应用。一起,走进智能体的新世界。
293 28
|
20天前
|
云栖大会
阿里云产品九月刊来啦
2025云栖大会重磅合集,阿里云各产品重大升级发布
116 23
|
14天前
|
Devops Shell Linux
【Azure Developer】使用Azure Developer CLI (azd)部署项目时候遇见无法登录中国区Azure的报错
使用Azure Developer CLI(azd)部署Aspire应用至Azure中国时,因1.20.0版本存在认证端点解析问题,导致登录失败。错误提示为OIDC发现URL不匹配。通过回滚至1.19.0版本并重新登录,可成功解决该问题。
169 14
|
28天前
|
存储 缓存 安全
《政企API网关:安全与性能平衡的转型实践》
本文记录某省政务数字化转型中API网关的重构实践。初代网关因安全策略粗放、性能与安全冲突等问题,出现权限越界风险、接口响应超300ms等问题。重构通过“RBAC+ABAC”混合鉴权实现细粒度安全管控,优化加密算法与鉴权缓存平衡安全与性能,搭建五维审计日志与第三方准入机制解决溯源和管控难题,还攻克鉴权缓存一致性等坑。最终权限拦截率达99.5%,接口响应缩至95ms,通过等保三级认证。
114 13
|
21天前
|
人工智能 IDE 程序员
Qoder 负责人揭秘:Qoder 产品背后的思考与未来发展
AI Coding 已经成为软件研发的必选项。根据行业的调研,目前全球超过 62% 的开发者正在使用 AI Coding 产品,开发者研发效率提升 30% 以上。当然,有很多开发者用得比较深入,提效超过 50%。
305 20
|
15天前
|
人工智能 运维 监控
当AI遇上自动化:运维测试终于不“加班”了
当AI遇上自动化:运维测试终于不“加班”了
151 9
|
23天前
|
人工智能 运维 Serverless
函数计算 × MSE Nacos : 轻松托管你的 MCP Server
本文将通过一个具体案例,演示如何基于 MCP Python SDK 开发一个标准的 MCP Server,并将其部署至函数计算。在不修改任何业务代码的前提下,通过控制台简单配置,即可实现该服务自动注册至 MSE Nacos 企业版,并支持后续的动态更新与统一管理。
370 35
下一篇
开通oss服务