经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » Go语言 » 查看文章
深入理解 sync.Once 与 sync.Pool
来源:cnblogs  作者:沉睡的木木夕  时间:2021/6/28 9:37:15  对本文有异议

深入理解 sync.Once 与 sync.Pool

sync.Once 代表在这个对象下在这个示例下多次执行能保证只会执行一次操作。

  1. var once sync.Once
  2. for i:=0; i < 10; i++ {
  3. once.Do(func(){
  4. fmt.Println("execed...")
  5. })
  6. }

在上面的例子中,once.Do 的参数 func 函数就会保证只执行一次。

sync.Once 原理

那么 sync.Once 是如何保证 Do 执行体函数只执行一次呢?

从 sync.Once 的源码就可以看出其实就是通过一个 uint32 类型的 done 标识实现的。当 done = 1 就标识着已经执行过了。Once 的源码非常简短

  1. package sync
  2. import (
  3. "sync/atomic"
  4. )
  5. type Once struct {
  6. done uint32
  7. m Mutex
  8. }
  9. func (o *Once) Do(f func()) {
  10. if atomic.LoadUint32(&o.done) == 0 {
  11. o.doSlow(f)
  12. }
  13. }
  14. func (o *Once) doSlow(f func()) {
  15. o.m.Lock()
  16. defer o.m.Unlock()
  17. if o.done == 0 {
  18. defer atomic.StoreUint32(&o.done, 1)
  19. f()
  20. }
  21. }

Do 方法内部用到了内存加载同步原语 atomic.LoadUint32done = 0 表示还没有执行,所以多个请求在 f 执行前都会进来执行 o.doSlow(f),然后通过互斥锁使保证多个请求只有一个才能成功执行,保证了 f 成功返回之后才会内存同步原语将 done 设置为 1。最后释放锁,后面的请求就因无法满足判断而退出。

如果仔细查看源代码中的注释就会发现 go 团队还解释了为什么没有使用 cas 这种同步原语实现。因为 sync.OnceDo(f) 在执行的时候要保证只有在 f 执行完之后 do 才返回。想象一下有至少两个请求,Do 是用 cas 实现的:

  1. func (o *Once) Do(f func()) {
  2. if atomic.CompareAndSwapUint32(&o.done, 0, 1) {
  3. f()
  4. }
  5. }

虽然 cas 保证了同一时刻只有一个请求进入 if 判断执行 f()。但是其它的请求却没有等待 f() 执行完成就立即返回了。那么用户端在执行 once.Do 返回之后其实就可能存在 f() 还未完成,就会出现意料之外的错误。如下面例子

  1. var db SqlDb
  2. var once sync.Once
  3. for i:=0; i < 2; i++ {
  4. once.Do(func() {
  5. db = NewSqlDB()
  6. fmt.Println("execed...")
  7. })
  8. }
  9. // #1
  10. db.Query("select * from table")
  11. ...

根据上述如果是用 cas 实现的 once,那么当 once.Do 执行完返回并且循环体结束到达 #1 时,由于 db 的初始化函数可能还没完成,那么这个时候 db 还是 nil,那么直接调用 db.Query 就会发生错误了。

sync.Once 使用限制

由于 Go 语言一切皆 struct 的特性,我们在使用 sync.Once 的时候一定要注意不要通过传递参数使用。因为 go 对于 sync.Once 参数传递是值传递,会将原来的 once 拷贝过来,所以有可能会导致 once 会重复执行或者是已经执行过了就不会执行的问题。

  1. func main() {
  2. for i := 0; i < 10; i++ {
  3. once.Do(func() {
  4. fmt.Println("execed...")
  5. })
  6. }
  7. duplicate(once)
  8. }
  9. func duplicate(once sync.Once) {
  10. for i := 0; i < 10; i++ {
  11. once.Do(func() {
  12. fmt.Println("execed2...")
  13. })
  14. }
  15. }

比如上述例子,由于 once 已经执行过一次,once.done 已经为 1。这个时候再通过传递,由于 once.done 已经为1,所以就不会执行了。上面的输出结果只会打印第一段循环的结果 execed...

sync.Pool

sync.Pool 其实把初始化的对象放到内部的一个池对象中,等下次访问就直接返回池中的对象,如果没有的话就会生成这个对象放入池中。Pool 的目的是”预热“,即初始化但还未立即使用的对象,由于预先初始化至 Pool,所以到后续取得时候就直接返回已经初始化过得对象即可。这样提高了程序吞吐,因为有时候在运行时初始化一些对象的开销是非常昂贵的,如数据库连接对象等。

现在我们来深入分析 Pool

sync.Pool 原理

sync.Pool 核心对象有三个

  1. New:函数,负责对象初始化
  2. Get:获取 Pool 中的对象,如果 Pool 中对象不存在则会调用 New
  3. Put:将对象放入 Pool 中

New func

Pool 的结构很简单,就 5 个字段

  1. type Pool struct {
  2. ...
  3. New func() interface{}
  4. }

字段 New 是一个初始化对象的指针,该方法不是必填的,当没有设置 New 函数时,调用 Get 方法会返回 nil。只有在指定了 New 函数体后,调用 Get 如果发现 Pool 中没有就会调用 New 初始化方法并返回该对象。

poolLocalInternal

在将 Get、Put 之前得先了解 poolLocalInternal 这个对象,里面只有两个对象,都是用来存储要用的对象的:

  1. type poolLocalInternal struct {
  2. private interface{} // Can be used only by the respective P.
  3. shared poolChain // Local P can pushHead/popHead; any P can popTail.
  4. }

操作这个对象时必须要把当前的 goroutine 绑定到 P,并且禁止让出 g。在 Get 和 Put 操作时都是优先操作 private 这个字段,只有在这个字段为 nil 的情况下才会转而读取 poolChain 共享链表,每读取操作都是一次 pop。

Get

每个当前 goroutine 都拥有一个 poolLocalInternal.private,在 g 调用 Get 方法时会做如下方法:

  1. 查询 private 是否有值,有直接返回;没有查询共享 poolChain 链表
  2. 如果 poolChain 链表 pop 返回的值不为 nil,则直接返回;如果没有值则转向其它 P 中的 poolChain 队列中存在的值
  3. 如果其它的 P 的共享队列中都没有值,就会尝试在主存中地址获取对应的值返回
  4. 最终都没有就会执行 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

原文链接:http://www.cnblogs.com/ms27946/p/SyncOnce-And-SyncPool-For-Golang.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号