深入理解 sync.Once 与 sync.Pool
深入理解 sync.Once 与 sync.Pool
sync.Once
代表在这个对象下在这个示例下多次执行能保证只会执行一次操作。
var once sync.Once
for i:=0; i < 10; i++ {
once.Do(func(){
fmt.Println("execed...")
})
}
在上面的例子中,once.Do 的参数 func 函数就会保证只执行一次。
sync.Once 原理
那么 sync.Once 是如何保证 Do 执行体函数只执行一次呢?
从 sync.Once 的源码就可以看出其实就是通过一个 uint32 类型的 done 标识实现的。当 done = 1
就标识着已经执行过了。Once 的源码非常简短
package sync
import (
"sync/atomic"
)
type Once struct {
done uint32
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()
}
}
Do
方法内部用到了内存加载同步原语 atomic.LoadUint32
,done = 0
表示还没有执行,所以多个请求在 f
执行前都会进来执行 o.doSlow(f)
,然后通过互斥锁使保证多个请求只有一个才能成功执行,保证了 f 成功返回之后才会内存同步原语将 done
设置为 1。最后释放锁,后面的请求就因无法满足判断而退出。
如果仔细查看源代码中的注释就会发现 go 团队还解释了为什么没有使用 cas 这种同步原语实现。因为 sync.Once
的 Do(f)
在执行的时候要保证只有在 f 执行完之后 do 才返回。想象一下有至少两个请求,Do 是用 cas 实现的:
func (o *Once) Do(f func()) {
if atomic.CompareAndSwapUint32(&o.done, 0, 1) {
f()
}
}
虽然 cas 保证了同一时刻只有一个请求进入 if 判断执行 f()。但是其它的请求却没有等待 f() 执行完成就立即返回了。那么用户端在执行 once.Do 返回之后其实就可能存在 f() 还未完成,就会出现意料之外的错误。如下面例子
var db SqlDb
var once sync.Once
for i:=0; i < 2; i++ {
once.Do(func() {
db = NewSqlDB()
fmt.Println("execed...")
})
}
// #1
db.Query("select * from table")
...
根据上述如果是用 cas 实现的 once,那么当 once.Do
执行完返回并且循环体结束到达 #1 时,由于 db 的初始化函数可能还没完成,那么这个时候 db 还是 nil,那么直接调用 db.Query
就会发生错误了。
sync.Once 使用限制
由于 Go 语言一切皆 struct 的特性,我们在使用 sync.Once 的时候一定要注意不要通过传递参数使用。因为 go 对于 sync.Once 参数传递是值传递,会将原来的 once 拷贝过来,所以有可能会导致 once 会重复执行或者是已经执行过了就不会执行的问题。
func main() {
for i := 0; i < 10; i++ {
once.Do(func() {
fmt.Println("execed...")
})
}
duplicate(once)
}
func duplicate(once sync.Once) {
for i := 0; i < 10; i++ {
once.Do(func() {
fmt.Println("execed2...")
})
}
}
比如上述例子,由于 once 已经执行过一次,once.done 已经为 1。这个时候再通过传递,由于 once.done 已经为1,所以就不会执行了。上面的输出结果只会打印第一段循环的结果 execed...
。
sync.Pool
sync.Pool 其实把初始化的对象放到内部的一个池对象中,等下次访问就直接返回池中的对象,如果没有的话就会生成这个对象放入池中。Pool 的目的是”预热“,即初始化但还未立即使用的对象,由于预先初始化至 Pool,所以到后续取得时候就直接返回已经初始化过得对象即可。这样提高了程序吞吐,因为有时候在运行时初始化一些对象的开销是非常昂贵的,如数据库连接对象等。
现在我们来深入分析 Pool
sync.Pool 原理
sync.Pool 核心对象有三个
- New:函数,负责对象初始化
- Get:获取 Pool 中的对象,如果 Pool 中对象不存在则会调用 New
- Put:将对象放入 Pool 中
New func
Pool 的结构很简单,就 5 个字段
type Pool struct {
...
New func() interface{}
}
字段 New
是一个初始化对象的指针,该方法不是必填的,当没有设置 New 函数时,调用 Get 方法会返回 nil。只有在指定了 New 函数体后,调用 Get 如果发现 Pool 中没有就会调用 New 初始化方法并返回该对象。
poolLocalInternal
在将 Get、Put 之前得先了解 poolLocalInternal 这个对象,里面只有两个对象,都是用来存储要用的对象的:
type poolLocalInternal struct {
private interface{} // Can be used only by the respective P.
shared poolChain // Local P can pushHead/popHead; any P can popTail.
}
操作这个对象时必须要把当前的 goroutine 绑定到 P,并且禁止让出 g。在 Get 和 Put 操作时都是优先操作 private
这个字段,只有在这个字段为 nil 的情况下才会转而读取 poolChain 共享链表,每读取操作都是一次 pop。
Get
每个当前 goroutine 都拥有一个 poolLocalInternal.private
,在 g 调用 Get 方法时会做如下方法:
- 查询
private
是否有值,有直接返回;没有查询共享 poolChain 链表 - 如果 poolChain 链表 pop 返回的值不为 nil,则直接返回;如果没有值则转向其它 P 中的 poolChain 队列中存在的值
- 如果其它的 P 的共享队列中都没有值,就会尝试在主存中地址获取对应的值返回
- 最终都没有就会执行 New 函数体返回,没有设置 New 则返回 nil。
从上面的调用过程来看,Pool.Get 获取值的过程在一定程度与 gmp 模型有很多相似的地方的。
Put
Put 操作就比较简单了,优先将值赋值给 poolLocalInternal.private
(同样是固定将当前的 G 绑定到 P 上),如果同时有多个值 Put,那么就会将剩余的值插入到共享链表 poolChain
sync.Pool 使用限制
因为 pool 每次的 get 操作都会将值 remove + return
,相当于用完即抛。并且要注意 Get 的执行过程。Put 方法的参数类型可以是任意类型,一定要切记不要将不同类型的值存进去。如果存在多协程(或循环)调用 Get 时,你无法确定哪次调用的就是你想要的类型而导致出现未知的错误。
文本同步至:https://github.com/MarsonShine/GolangStudy/issues/5
深入理解 sync.Once 与 sync.Pool的更多相关文章
- 理解LGWR,Log File Sync Waits以及Commit的性能问题[转]
理解LGWR,Log File Sync Waits以及Commit的性能问题 一.概要: 1. Commit和log filesync的工作机制 2. 为什么log file wait太久 3. ...
- 理解vue 修饰符sync
也是在vux中看到了这个sync 现在我们来看看vue中的sync 我们先看下官方文档:vue .sync 修饰符,里面说vue .sync 修饰符以前存在于vue1.0版本里,但是在在 2.0 中移 ...
- 深入理解vue 修饰符sync
[ vue sync修饰符示例] 在说vue 修饰符sync前,我们先看下官方文档:vue .sync 修饰符,里面说vue .sync 修饰符以前存在于vue1.0版本里,但是在在 2.0 中移除了 ...
- 深入理解vue 修饰符sync【 vue sync修饰符示例】
在说vue 修饰符sync前,我们先看下官方文档:vue .sync 修饰符,里面说vue .sync 修饰符以前存在于vue1.0版本里,但是在在 2.0 中移除了 .sync .但是在 2.0 发 ...
- 理解Java String和String Pool
本文转载自: http://blog.sina.com.cn/s/blog_5203f6ce0100tiux.html 要理解 java中String的运作方式,必须明确一点:String是一个非可变 ...
- sync.WaitGroup和sync.Once
sync.WaitGroup,顾名思义,等待一组goroutinue运行完毕.sync.WaitGroup声明后即可使用,它有如下方法: func (wg *WaitGroup) Add(delta ...
- golang中sync.RWMutex和sync.Mutex区别
golang中sync包实现了两种锁Mutex (互斥锁)和RWMutex(读写锁),其中RWMutex是基于Mutex实现的,只读锁的实现使用类似引用计数器的功能. type Mutex f ...
- Go 初体验 - 并发与锁.1 - sync.Mutex 与 sync.RWMutex
==== Mutex为互斥锁,顾名思义,被Mutex锁住的代码同时只允许一个协程访问,其它协程进来就要排队 如何使用?看代码: 输出: 释义: 并发1000个协程同时更改m的元素,这样会有一部分更改成 ...
- 深入理解.sync修饰符
原文地址:http://www.geeee.top/2019/04/17/vue-sync/ 转载请注明出处 .sync修饰符 一个组件上只能定义一个v-model,如果其他prop也要实现双向绑定的 ...
随机推荐
- 改善c++程序的150个建议(读后总结)-------12-18
12.优先使用前置操作符 #include <iostream> using namespace std; class A { private: int num; public: A op ...
- 再谈vbo
我们之前都是通过glNamedBufferData初始化buffer object,初始化的意思是为buffer object开辟显存空间,并填充数据: GLfloat position[] = { ...
- BUAA软件工程个人项目
写在前面 项目 内容 所属课程 2020春季计算机学院软件工程(罗杰 任健) (北航) 作业要求 [个人项目作业](<https://edu.cnblogs.com/campus/buaa/BU ...
- 【近取 Key】Alpha - v1.0 版本发布说明
功能与特性 Alpha 版本虽然为本软件的第一代版本,但已基本覆盖了用户个人使用时的主要功能.除登陆注册与后台管理外,下文将分版块详细介绍面向用户的主要功能特性. 『产品主页』 潜在应用场景 场景 0 ...
- 25.Qt Quick QML-500行代码实现"合成大西瓜游戏"
"合成大西瓜"这个游戏在年前很火热,还上过微博热搜,最近便玩了一阵还挺有意思的,所以研究了一下小球碰撞原理,自己亲自手写碰撞算法来实现一个合成大西瓜游戏.并支持任意大小布局,你想玩 ...
- [其他] vscode 快速教程
概述 vs:集成开发环境,包括软件生命周期中需要的大部分工具,如UML,代码管控,IDE等 vs code:代码编辑器,支持插件扩展,对网页和云端开发做了优化 快捷键 F1/ctrl+shift+p: ...
- [BD] Sqoop
什么是Sqoop 数据交换工具(ETL):RDBMS(MySQL.Oracle等)<-->Sqoop<-->HDFS(HBase.Hive等) 基于JDBC 执行数据交换时,本 ...
- Mysql体系结构管理
1.客户端与服务端模型 1)数据是一个典型的C/S结构的服务 1.mysql自带的客户端工具 mysql mysqladmin mysqldump 3.mysql是一个二进制程序,后台守护进程 单进程 ...
- Linux_配置匿名访问FTP服务
[RHEL8]-FTPserver:[Centos7]-FTPclient !!!测试环境我们首关闭防火墙和selinux(FTPserver和FTPclient都需要) [root@localhos ...
- 配置文件修改java安全级别和站点信息
配置文件修改java安全级别和站点信息原创Green_1001 最后发布于2015-04-22 23:00:09 阅读数 516 收藏展开 通过配置文件修改java安全级别 配置文件名称为deploy ...