Go并发编程之sync.Once使用实例详解

作者:深度思维者 时间:2024-04-26 17:21:23 

一.序

单从库名大概就能猜出其作用。sync.Once使用起来很简单, 下面是一个简单的使用案例


package main

import (
"fmt"
"sync"
)

func main() {
var (
once sync.Once
wg   sync.WaitGroup
)

for i := 0; i < 10; i++ {
wg.Add(1)
// 这里要注意讲i显示的当参数传入内部的匿名函数
go func(i int) {
defer wg.Done()
// fmt.Println("once", i)
once.Do(func() {
fmt.Println("once", i)
})
}(i)
}

wg.Wait()
fmt.Printf("over")
}

输出:

❯ go run ./demo.go
once 9

测试如果不添加once.Do 这段代码,则会输出如下结果,并且每次执行的输出都不一样。

once 9
once 0
once 3
once 6
once 4
once 1
once 5
once 2
once 7
once 8

从两次输出不同,我们可以得知 sync.Once的作用是:保证传入的函数只执行一次

二. 源码分析

2.1结构体

Once的结构体如下


type Once struct {
   done uint32
   m    Mutex
}

每一个 sync.Once 结构体中都只包含一个用于标识代码块是否执行过的 done 以及一个互斥锁 sync.Mutex

2.2 接口

sync.Once.Dosync.Once 结构体对外唯一暴露的方法,该方法会接收一个入参为空的函数:

  • 如果传入的函数已经执行过,会直接返回

  • 如果传入的函数没有执行过, 会调用sync.Once.doSlow执行传入的参数


func (o *Once) Do(f func()) {
// Note: Here is an incorrect implementation of Do:
//
//if atomic.CompareAndSwapUint32(&o.done, 0, 1) {
//f()
//}
//
// Do guarantees that when it returns, f has finished.
// This implementation would not implement that guarantee:
// given two simultaneous calls, the winner of the cas would
// call f, and the second would return immediately, without
// waiting for the first's call to f to complete.
// This is why the slow path falls back to a mutex, and why
// the atomic.StoreUint32 must be delayed until after f returns.

if atomic.LoadUint32(&o.done) == 0 {
// Outlined slow-path to allow inlining of the fast-path.
o.doSlow(f)
}
}

代码注释 * 别给了一个说明: 很容易犯错的一种实现


if atomic.CompareAndSwapUint32(&o.done, 0, 1) {
f()
}

如果这么实现最大的问题是,如果并发调用,一个 goroutine 执行,另外一个不会等正在执行的这个成功之后返回,而是直接就返回了,这就不能保证传入的方法一定会先执行一次了

正确的实现方式


if atomic.LoadUint32(&o.done) == 0 {
   // Outlined slow-path to allow inlining of the fast-path.
   o.doSlow(f)
}

会先判断 done 是否为 0,如果不为 0 说明还没执行过,就进入 doSlow


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

doSlow 当中使用了互斥锁来保证只会执行一次

具体的逻辑

  • 为当前Goroutine获取互斥锁

  • 执行传入的无入参函数;

  • 运行延迟函数, 将成员变量done更新为1

三. 使用场景案例

3.1 单例模式

原子操作配合互斥锁可以实现非常高效的单件模式。互斥锁的代价比普通整数的原子读写高很多,在性能敏感的地方可以增加一个数字型的标志位,通过原子检测标志位状态降低互斥锁的使用次数来提高性能。


type singleton struct {}

var (
   instance    *singleton
   initialized uint32
   mu          sync.Mutex
)

func Instance() *singleton {
   if atomic.LoadUint32(&initialized) == 1 {
       return instance
   }

mu.Lock()
   defer mu.Unlock()

if instance == nil {
       defer atomic.StoreUint32(&initialized, 1)
       instance = &singleton{}
   }
   return instance
}

而使用sync.Once能更简单实现单例模式


type singleton struct {}

var (
   instance *singleton
   once     sync.Once
)

func Instance() *singleton {
   once.Do(func() {
       instance = &singleton{}
   })
   return instance
}

3.2 加载配置文件示例

延迟一个开销很大的初始化操作到真正用到它的时候再执行是一个很好的实践。因为预先初始化一个变量(比如在init函数中完成初始化)会增加程序的启动耗时,而且有可能实际执行过程中这个变量没有用上,那么这个初始化操作就不是必须要做的。我们来看一个例子:


var icons map[string]image.Image

func loadIcons() {
   icons = map[string]image.Image{
       "left":  loadIcon("left.png"),
       "up":    loadIcon("up.png"),
       "right": loadIcon("right.png"),
       "down":  loadIcon("down.png"),
   }
}

// Icon 被多个goroutine调用时不是并发安全的
// 因为map类型本就不是类型安全数据结构
func Icon(name string) image.Image {
   if icons == nil {
       loadIcons()
   }
   return icons[name]
}

多个goroutine并发调用Icon函数时不是并发安全的,编译器和CPU可能会在保证每个goroutine都满足串行一致的基础上自由地重排访问内存的顺序。loadIcons函数可能会被重排为以下结果:

func loadIcons() {
    icons = make(map[string]image.Image)
    icons["left"] = loadIcon("left.png")
    icons["up"] = loadIcon("up.png")
    icons["right"] = loadIcon("right.png")
    icons["down"] = loadIcon("down.png")
}

在这种情况下就会出现即使判断了icons不是nil也不意味着变量初始化完成了。考虑到这种情况,我们能想到的办法就是添加互斥锁,保证初始化icons的时候不会被其他的goroutine操作,但是这样做又会引发性能问题。

可以使用sync.Once 改造代码


var icons map[string]image.Image

var loadIconsOnce sync.Once

func loadIcons() {
   icons = map[string]image.Image{
       "left":  loadIcon("left.png"),
       "up":    loadIcon("up.png"),
       "right": loadIcon("right.png"),
       "down":  loadIcon("down.png"),
   }
}

// Icon 是并发安全的,并且保证了在代码运行的时候才会加载配置
func Icon(name string) image.Image {
   loadIconsOnce.Do(loadIcons)
   return icons[name]
}

这样设计就能保证初始化操作的时候是并发安全的并且初始化操作也不会被执行多次。

四.总结

作为用于保证函数执行次数的 sync.Once 结构体,它使用互斥锁和 sync/atomic 包提供的方法实现了某个函数在程序运行期间只能执行一次的语义。在使用该结构体时,我们也需要注意以下的问题:

  • sync.Once.Do 方法中传入的函数只会被执行一次,哪怕函数中发生了 panic;

  • 两次调用 sync.Once.Do 方法传入不同的函数只会执行第一次调传入的函数;

五. 参考

  • https://lailin.xyz/post/go-training-week3-once.html

  • https://www.topgoer.cn/docs/gozhuanjia/chapter055.2-waitgroup

  • https://www.topgoer.com/并发编程/sync.html

  • https://chai2010.cn/advanced-go-programming-book/ch1-basic/ch1-05-mem.html

来源:https://www.cnblogs.com/failymao/p/15501775.html

标签:Go,并发编程,sync.Once
0
投稿

猜你喜欢

  • Python语法学习之正则表达式的量词汇总

    2023-07-20 03:08:59
  • python Selenium实现付费音乐批量下载的实现方法

    2023-02-28 17:26:58
  • mysql如何配置白名单访问

    2024-01-25 15:43:20
  • python使用webdriver爬取微信公众号

    2022-10-06 02:13:30
  • VMware中安装CentOS7(设置静态IP地址)并通过docker容器安装mySql数据库(超详细教程)

    2024-01-14 02:58:23
  • python字符串大小写转换的三种方法

    2021-11-09 15:04:51
  • python2.7实现FTP文件下载功能

    2021-10-09 19:04:37
  • AJAX的jQuery实现入门(二)

    2008-05-01 13:04:00
  • 怎么写好一份图形界面设计师简历

    2009-04-16 13:10:00
  • 进制转换算法原理(二进制 八进制 十进制 十六进制)

    2022-01-09 03:18:23
  • 利用pipenv和pyenv管理多个相互独立的Python虚拟开发环境

    2022-06-03 19:54:30
  • Python Flask-web表单使用详解

    2022-07-14 01:55:59
  • Python画图高斯分布的示例

    2023-02-07 09:09:14
  • pytorch 网络参数 weight bias 初始化详解

    2023-08-12 07:43:57
  • 深入浅析python定时杀进程

    2021-10-23 17:14:15
  • Python max函数中key的用法及原理解析

    2022-09-09 04:00:24
  • WEB前端开发规范文档

    2010-10-19 12:32:00
  • Python中Numpy的深拷贝和浅拷贝

    2021-10-04 21:08:43
  • SQL Server 2005恢复Master库

    2011-05-16 13:11:00
  • vue实现百度搜索下拉提示功能实例

    2024-04-28 09:31:15
  • asp之家 网络编程 m.aspxhome.com