七. Go并发编程--sync.Once
一.序
单从库名大概就能猜出其作用。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.Do 是 sync.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
七. Go并发编程--sync.Once的更多相关文章
- 十二. Go并发编程--sync/errGroup
一.序 这一篇算是并发编程的一个补充,起因是当前有个项目,大概の 需求是,根据kafka的分区(partition)数,创建同等数量的 消费者( goroutine)从不同的分区中消费者消费数据,但是 ...
- Java多线程学习(七)并发编程中一些问题
本节思维导图: 关注微信公众号:"Java面试通关手册" 回复"Java多线程"获取思维导图源文件和思维导图软件. 多线程就一定好吗?快吗?? 并发编程的目的就 ...
- [书籍翻译] 《JavaScript并发编程》第七章 抽取并发逻辑
本文是我翻译<JavaScript Concurrency>书籍的第七章 抽取并发逻辑,该书主要以Promises.Generator.Web workers等技术来讲解JavaScrip ...
- Python并发编程之从生成器使用入门协程(七)
大家好,并发编程 进入第七篇. 从今天开始,我们将开始进入Python的难点,那就是协程. 为了写明白协程的知识点,我查阅了网上的很多相关资料.发现很难有一个讲得系统,讲得全面的文章,导致我们在学习的 ...
- 并发编程(七)——AbstractQueuedSynchronizer 之 CountDownLatch、CyclicBarrier、Semaphore 源码分析
这篇,我们的关注点是 AQS 最后的部分,共享模式的使用.本文先用 CountDownLatch 将共享模式说清楚,然后顺着把其他 AQS 相关的类 CyclicBarrier.Semaphore 的 ...
- java并发编程笔记(七)——线程池
java并发编程笔记(七)--线程池 new Thread弊端 每次new Thread新建对象,性能差 线程缺乏统一管理,可能无限制的新建线程,相互竞争,有可能占用过多系统资源导致死机或者OOM 缺 ...
- java并发编程工具类JUC第七篇:BlockingDeque双端阻塞队列
在之前的文章中已经为大家介绍了java并发编程的工具:BlockingQueue接口.ArrayBlockingQueue.DelayQueue.LinkedBlockingQueue.Priorit ...
- 1、网络并发编程--简介、软件开发架构、OSI七层协议
python复习 变量与常量 基本数据类型 内置方法 字符编码.文件操作 函数 函数参数.闭包函数.装饰器 面向对象 封装.继承.多态 """ 什么是对象 数据与功能的结 ...
- java并发编程(七)synchronized详解
Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码. 一.当两个并发线程访问同一个对象object中的这个synchronized( ...
随机推荐
- pygame简单小游戏 移动小球
键盘a,d,s,w移动小球 需要安装pygame cmd里输入pip install pygame import pygame import sys pygame.init() screen = py ...
- Windows 11抢先体验
SHA1值: 3B6DA9194BA303AC7DBBF2E521716C809500919C 谷歌云:https://drive.google.com/file/d/1sH0cBI9hwh8EdlV ...
- Docker安装flink及避坑指南
Docker安装flink 导航 无处不在的大数据 安装flink 拉取flink镜像 编写docker-compose.yml 生成启动 查看安装效果 常见坑及解决方案 问题1 问题2 参考 本 ...
- Linux从头学13:想彻底搞懂“系统调用”的底层原理?建议您别错过这篇【调用门】
作 者:道哥,10+年嵌入式开发老兵,专注于:C/C++.嵌入式.Linux. 关注下方公众号,回复[书籍],获取 Linux.嵌入式领域经典书籍:回复[PDF],获取所有原创文章( PDF 格式). ...
- mysql数据库备份参数
我用来实现自动全备份的脚本(可以满足一般有前后版本兼容要求的导出导入操作,我的字符集是latin1): mysqldump.exe -umyusername -pmypass -h localhost ...
- 使用ELK监控Nginx日志实现接口流量访问统计
前段时间自己看书学习了一下elasticSearch,后面自己实践了使用elasticSearch.logStash.kibana搭建一个网站接口流量访问统计的监控看板.在这里做一些记录学习. 先看一 ...
- Java基础系列(32)- 递归讲解
递归 A方法调用B方法,我们很容易理解 递归就是:A方法调用A方法!就是自己调用自己 利用递归可以用简单的程序来解决一些复杂的问题.它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题 ...
- django使用celery搭配redis配置定时任务
已经安装环境: Python3.6 django==2.1.8(用2.2.2需要升级sqlite3) 项目名称:ceshiproject APP名称:ceshi 第一步:centos7下首先安装r ...
- 最详细STL(一)vector
vector的本质还是数组,但是可以动态的增加和减少数组的容量(当数组空间内存不足时,都会执行: 分配新空间-复制元素-释放原空间),首先先讲讲vector和数组的具体区别 一.vector和数组的区 ...
- mysql面试题及答案,mysql最新面试题,mysql面试题大全汇总
mysql最新面试题及答案汇总 Mysql 的存储引擎,myisam和innodb的区别.mysql最新面试题 答: 1.MyISAM 是非事务的存储引擎,适合用于频繁查询的应用.表锁,不会出现死锁, ...