文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

一文解析Golangsync.Once用法及原理

2024-04-02 19:55

关注

前言

在此前一篇文章中我们了解了 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 方法有哪些需要注意的:

func main() {
 var once sync.Once
 once.Do(func() {
    once.Do(func() {
       fmt.Println("hello kenmawr.")
    })
 })
}

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()
}

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()
   }
}

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

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减少了一次偏移量的计算,访问速度更快。

其实使用 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()
   }
}

5. 避坑

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

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯