一文解析 Golang sync.Once 用法及原理

目录
  • 前言
  • 1. 定位
  • 2. 对外接口
  • 3. 实战用法
    • 3.1 初始化
    • 3.2 单例模式
    • 3.3 关闭channel
  • 4. 原理
  • 5. 避坑

前言

在此前一篇文章中我们了解了 Golang Mutex 原理解析,今天来看一个官方给出的 Mutex 应用场景:sync.Once。

1. 定位

Once is an object that will perform exactly one action.

sync.Once 是 Go 标准库提供的使函数只执行一次的实现,常应用于单例模式,例如初始化配置、保持数据库连接等。它可以在代码的任意位置初始化和调用,因此可以延迟到使用时再执行,并发场景下是线程安全的。

2. 对外接口

Once 对外仅暴露了唯一的方法 Do(f func()),f 为需要执行的函数。

// Do calls the function f if and only if Do is being called for the
// first time for this instance of Once. In other words, given
// 	var once Once
// if once.Do(f) is called multiple times, only the first call will invoke f,
// even if f has a different value in each invocation. A new instance of
// Once is required for each function to execute.
//
// Do is intended for initialization that must be run exactly once. Since f
// is niladic, it may be necessary to use a function literal to capture the
// arguments to a function to be invoked by Do:
// 	config.once.Do(func() { config.init(filename) })
//
// Because no call to Do returns until the one call to f returns, if f causes
// Do to be called, it will deadlock.
//
// If f panics, Do considers it to have returned; future calls of Do return
// without calling f.
//
func (o *Once) Do(f func()) 

结合注释,我们来看看 Do 方法有哪些需要注意的:

  • 只有在当前的 Once 实例第一次调用 Do 方法时,才会真正执行 f。哪怕在多次调用 Do 中间 f 的值有所变化,也只会被实际调用一次;
  • Do 针对的是只希望执行一次的初始化操作,由于f 是没有参数的,如果需要传参,可以采用包装一层 func 的形式来实现:config.once.Do(func() { config.init(filename) })
  • 在对f 的调用返回之前,不会返回对Do的调用,所以如果f方法中又调用来Do方法,将会死锁。所以不要做这样的操作:
func main() {
 var once sync.Once
 once.Do(func() {
    once.Do(func() {
       fmt.Println("hello kenmawr.")
    })
 })
}
  • 如果 f 抛出了 panic,此时Do会认为f已经返回,后续多次调用Do也不会再触发对 f 的调用。

3. 实战用法

sync.Once 的场景很多,但万变不离其宗的落脚点在于:任何只希望执行一次的操作

基于此,我们可以发现很多具体的应用场景落地,比如某个资源的清理,全局变量的初始化,单例模式等,它们本质都是一样的。这里简单列几个,大家可以直接参考代码熟悉。

3.1 初始化

很多同学可能会有疑问,我直接在 init() 函数里面做初始化不就可以了吗?效果上是一样的,为什么还要用 sync.Once,这样还需要多声明一个 once 对象。

原因在于:init() 函数是在所在包首次被加载时执行,若未实际使用,既浪费了内存,又延缓了程序启动时间。而 sync.Once 可以在任何位置调用,而且是并发安全的,我们可以在实际依赖某个变量时才去初始化,这样「延迟初始化」从功能上讲并无差异,但可以有效地减少不必要的性能浪费。

我们来看 Golang 官方的 html 库中的一个例子,我们经常使用的转义字符串函数

func UnescapeString(s string) string

在进入函数的时候,首先就会依赖包里内置的 populateMapsOnce 实例(本质是一个 sync.Once) 来执行初始化 entity 的操作。这里的entity是一个包含上千键值对的 map,如果init()时就去初始化,会浪费内存。

var populateMapsOnce sync.Once
var entity           map[string]rune

func populateMaps() {
    entity = map[string]rune{
        "AElig;":                           '\U000000C6',
        "AMP;":                             '\U00000026',
        "Aacute;":                          '\U000000C1',
        "Abreve;":                          '\U00000102',
        "Acirc;":                           '\U000000C2',
        // 省略后续键值对
    }
}

func UnescapeString(s string) string {
    populateMapsOnce.Do(populateMaps)
    i := strings.IndexByte(s, '&')

    if i < 0 {
            return s
    }
    // 省略后续的实现
    ...
}

3.2 单例模式

开发中我们经常会实现 Getter 来暴露某个非导出的变量,这个时候就可以把 once.Do 放到 Getter 里面,完成单例的创建。

package main
import (
   "fmt"
   "sync"
)
type Singleton struct{}
var singleton *Singleton
var once sync.Once

func GetSingletonObj() *Singleton {
   once.Do(func() {
      fmt.Println("Create Obj")
      singleton = new(Singleton)
   })
   return singleton
}
func main() {
   var wg sync.WaitGroup
   for i := 0; i < 5; i++ {
      wg.Add(1)
      go func() {
         defer wg.Done()
         obj := GetSingletonObj()
         fmt.Printf("%p\n", obj)
      }()
   }
   wg.Wait()
}
/*--------- 输出 -----------
Create Obj
0x119f428
0x119f428
0x119f428
0x119f428
0x119f428
**/

3.3 关闭channel

一个channel如果已经被关闭,再去关闭的话会 panic,此时就可以应用 sync.Once 来帮忙。

type T int
type MyChannel struct {
   c    chan T
   once sync.Once
}
func (m *MyChannel) SafeClose() {
   // 保证只关闭一次channel
   m.once.Do(func() {
      close(m.c)
   })
}

4. 原理

在 sync 的源码包中,Once 的定义是一个 struct,所有定义和实现去掉注释后不过 30行,我们直接上源码来分析:

package sync
import (
   "sync/atomic"
)
// 一个 Once 实例在使用之后不能被拷贝继续使用
type Once struct {
   done uint32 // done 表明了动作是否已经执行
   m    Mutex
}
func (o *Once) Do(f func()) {
    if atomic.LoadUint32(&o.done) == 0 {
      o.doSlow(f)
   }
}

func (o *Once) doSlow(f func()) {
   o.m.Lock()
   defer o.m.Unlock()
   if o.done == 0 {
      defer atomic.StoreUint32(&o.done, 1)
      f()
   }
}

这里有两个非常巧妙的设计值得学习,我们参照注释来看一下:

  • 结构体字段顺序对速度的影响 我们来看一下带注释的 Once 结构定义
type Once struct {
	// done indicates whether the action has been performed.
	// It is first in the struct because it is used in the hot path.
	// The hot path is inlined at every call site.
	// Placing done first allows more compact instructions on some architectures (amd64/386),
	// and fewer instructions (to calculate offset) on other architectures.
	done uint32
	m    Mutex
}

sync.Once绝大多数场景都会访问o.done,访问 done 的机器指令是处于hot path上,hot path表示程序非常频繁执行的一系列指令。由于结构体第一个字段的地址和结构体的指针是相同的,如果是第一个字段,直接对结构体的指针解引用即可,如果是其他的字段,除了结构体指针外,还需要计算与第一个值的偏移,所以将done放在第一个字段,则CPU减少了一次偏移量的计算,访问速度更快。

  • 为何不使用 CAS 来达到执行一次的效果

其实使用 atomic.CompareAndSwapUint32 是一个非常直观的方案,这样的话 Do 的实现就变成了

func (o *OnceA) Do(f func()) {
  if !atomic.CompareAndSwapUint32(&o.done, 0, 1) {
    return
  }
  f()
}

这样的问题在于,一旦出现 CAS 失败的情况,成功协程会继续执行 f,但其他失败协程不会等待 f 执行结束。而Do 的API定位对此有着强要求,当一次 once.Do 返回时,执行的 f 一定是完成的状态。

对此,sync.Once 官方给出的解法是:

Slow path falls back to a mutex, and the atomic.StoreUint32 must be delayed until after f returns.

我们再来结合 doSlow() 看一看这里是怎么解决这个并发问题的:

func (o *Once) Do(f func()) {
    if atomic.LoadUint32(&o.done) == 0 {
      o.doSlow(f)
   }
}
func (o *Once) doSlow(f func()) {
   o.m.Lock()
   defer o.m.Unlock()
   if o.done == 0 {
      defer atomic.StoreUint32(&o.done, 1)
      f()
   }
}
  • atomic.LoadUint32 用于原子加载地址(也就是 &o.done),返回加载到的值;
  • o.done 为 0 是代表尚未执行。若同时有两个 goroutine 进来,发现 o.done 为 0(此时 f 尚未执行),就会进入 o.doSlow(f) 的慢路径中(slow path);
  • doSlow 使用 sync.Mutex 来加锁,一个协程进去,其他的被阻塞在锁的地方(注意,此时是阻塞,不是直接返回,这是和 CAS 方案最大的差别);
  • 经过 o.m.Lock() 获取到锁以后,如果此时 o.done 还是 0,意味着依然没有被执行,此时就可以放心的调用 f来执行了。否则,说明当前协程在被阻塞的过程中,已经失去了调用f 的机会,直接返回。
  • defer atomic.StoreUint32(&o.done, 1) 是这里的精华,必须等到f() 返回,在 defer 里才能够去更新 o.done 的值为 1。

5. 避坑

  • 不要拷贝一个 sync.Once 使用或作为参数传递,然后去执行 Do,值传递时 done 会归0,无法起到限制一次的效果。
  • 不要在 Do 的 f 中嵌套调用 Do

到此这篇关于一文解析 Golang sync.Once 用法及原理的文章就介绍到这了,更多相关 Golang sync.Once 内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Golang之sync.Pool使用详解

    前言 我们通常用 Golang 来开发并构建高并发场景下的服务,但是由于 Golang 内建的GC机制多少会影响服务的性能,因此,为了减少频繁GC,Golang提供了对象重用的机制,也就是使用sync.Pool构建对象池. sync.Pool介绍 首先sync.Pool是可伸缩的临时对象池,也是并发安全的.其可伸缩的大小会受限于内存的大小,可以理解为是一个存放可重用对象的容器.sync.Pool设计的目的就是用于存放已经分配的但是暂时又不用的对象,而且在需要用到的时候,可以直接从该pool中取.

  • golang中sync.Mutex的实现方法

    目录 mutex 的实现思想 golang 中 mutex 的实现思想 mutex 的结构以及一些 const 常量值 Mutex 没有被锁住,第一个协程来拿锁 Mutex 仅被协程 A 锁住,没有其他协程抢锁,协程 A 释放锁 Mutex 已经被协程 A 锁住,协程 B 来拿锁 lockSlow() runtime_doSpin() runtime_canSpin() Mutex 被协程 A 锁住,协程 B 来抢锁但失败被放入等待队列,此时协程 A 释放锁 unlockSlow() Mutex

  • golang中使用sync.Map的方法

    背景 go中map数据结构不是线程安全的,即多个goroutine同时操作一个map,则会报错,因此go1.9之后诞生了sync.Map sync.Map思路来自java的ConcurrentHashMap 接口 sync.map就是1.9版本带的线程安全map,主要有如下几种方法: Load(key interface{}) (value interface{}, ok bool) //通过提供一个键key,查找对应的值value,如果不存在,则返回nil.ok的结果表示是否在map中找到值

  • 在golang中使用Sync.WaitGroup解决等待的问题

    面对goroutine我们都需要等待它完成交给它的事情,等待它计算完成或是执行完毕,所以不得不在程序需要等待的地方使用time.Sleep()来睡眠一段时间,等待其他goroytine执行完毕,下面的代码打印1到100的for循环可以在很快的时间内运行完毕,但是我们必须添加time.Sleep()来等待其打印完毕,如果我们不等待仿佛什么也没有发生一样.....这肯定不是我们想要的! func main(){ for i := 0; i < 100 ; i++{ go fmt.Println(i)

  • 一文解析 Golang sync.Once 用法及原理

    目录 前言 1. 定位 2. 对外接口 3. 实战用法 3.1 初始化 3.2 单例模式 3.3 关闭channel 4. 原理 5. 避坑 前言 在此前一篇文章中我们了解了 Golang Mutex 原理解析,今天来看一个官方给出的 Mutex 应用场景:sync.Once. 1. 定位 Once is an object that will perform exactly one action. sync.Once 是 Go 标准库提供的使函数只执行一次的实现,常应用于单例模式,例如初始化配

  • GoLang sync.Pool简介与用法

    目录 使用场景 使用方法 声明对象池 Get & Put 性能测试 使用场景 一句话总结:保存和复用临时对象,减少内存分配,降低GC压力 sync.Pool是可伸缩的,也是并发安全的,其大小仅受限于内存大小.sync.Pool用于存储那些被分配了但是没有使用,而未来可能会使用的值.这样就可以不用再次经过内存分配,可直接复用已有对象,减轻GC的压力,从而提升系统性能. 使用方法 声明对象池 type Student struct { Name string Age int32 Remark [10

  • Golang errgroup 设计及实现原理解析

    目录 开篇 errgroup 源码拆解 Group WithContext Wait Go SetLimit TryGo 使用方法 结束语 开篇 继上次学习了信号量 semaphore 扩展库的设计思路和实现之后,今天我们继续来看 golang.org/x/sync 包下的另一个经常被 Golang 开发者使用的大杀器:errgroup. 业务研发中我们经常会遇到需要调用多个下游的场景,比如加载一个商品的详情页,你可能需要访问商品服务,库存服务,券服务,用户服务等,才能从各个数据源获取到所需要的

  • json格式解析和libjson的用法介绍(关于cjson的使用方法)

    在阅读本文之前,请先阅读下<Rss Reader实例开发之系统设计>一文. Rss Reader实例开发中,进行网络数据交换时主要使用到了两种数据格式:JSON与XML.本文主要介绍JSON格式的简单概念及JSON在Rss Reader中的应用,XML格式的使用将在下一篇文章做介绍. JSON简介: JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式,可以把JSON的结构理解成无序的.可嵌套的key-value键值对集合,这些key-value键值对是

  • Python 中 -m 的典型用法、原理解析与发展演变

    在命令行中使用 Python 时,它可以接收大约 20 个选项(option),语法格式如下: python [-bBdEhiIOqsSuvVWx?] [-c command | -m module-name | script | - ] [args] 本文想要聊聊比较特殊的"-m"选项: 关于它的典型用法.原理解析与发展演变的过程. 首先,让我们用"--help"来看看它的解释: -m mod run library module as a script (ter

  • PHP 范围解析操作符(::)用法分析【访问静态成员和类常量】

    本文实例讲述了PHP 范围解析操作符(::)用法.分享给大家供大家参考,具体如下: 范围解析操作符 (::) 范围解析操作符(也可称作 Paamayim Nekudotayim)或者更简单地说是一对冒号,可以用于访问静态成员,类常量,还可以用于覆盖类中的属性和方法. 当在类定义之外引用到这些项目时,要使用类名. 自 PHP 5.3.0 起,可以通过变量来引用类,该变量的值不能是关键字(如 self,parent 和 static). 把 Paamayim Nekudotayim 选作双冒号操作符

  • golang守护进程用法示例

    本文实例讲述了golang守护进程用法.分享给大家供大家参考,具体如下: 用node写了一个socket后台服务,可是有时候会挂,node一个异常就game over了,所以写了一个守候. 复制代码 代码如下: package main import (         "log"         "os"         "os/exec"         "time" ) func main() {         lf,

  • Java实现的获取和判断文件头信息工具类用法示例

    本文实例讲述了Java实现的获取和判断文件头信息工具类用法.分享给大家供大家参考,具体如下: package test; import java.io.FileInputStream; import java.io.IOException; import java.util.HashMap; /** * 获取和判断文件头信息 * * @author Sud * */ public class GetTypeByHead { // 缓存文件头信息-文件头信息 public static final

  • spring cloud Ribbon用法及原理解析

    这篇文章主要介绍了spring cloud Ribbon用法及原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 简介 这篇文章主要介绍一下ribbon在程序中的基本使用,在这里是单独拿出来写用例测试的,实际生产一般是配置feign一起使用,更加方便开发.同时这里也通过源码来简单分析一下ribbon的基本实现原理. 基本使用 这里使用基于zookeeper注册中心+ribbon的方式实现一个简单的客户端负载均衡案例. 服务提供方 首先是一个

  • java JSON解析库Alibaba Fastjson用法详解

    本文实例讲述了java JSON解析库Alibaba Fastjson用法.分享给大家供大家参考,具体如下: Json是一种轻量级的数据交换格式,应该在一个程序员的开发生涯中是常接触的.简洁和清晰的层次结构使得 JSON 成为理想的数据交换语言. 易于人阅读和编写,同时也易于机器解析和生成,并有效地提升网络传输效率. 现在主流的对象与 JSON 互转的工具很多,这里介绍的是阿里巴巴的开源库 - Fastjson.Fastjson是一个Java库,可用于将Java对象转换为其JSON表示.它还可用

随机推荐