go sync Once实现原理示例解析

目录
  • 正文
  • Once 的实现
  • 使用示例
  • Once 的一些工作机制
  • Once 详解
    • hotpath
    • atomic.LoadUint32
    • atomic.StoreUint32
    • Mutex
  • 总结

正文

在很多情况下,我们可能需要控制某一段代码只执行一次,比如做某些初始化操作,如初始化数据库连接等。 对于这种场景,go 为我们提供了 sync.Once 对象,它保证了某个动作只被执行一次。 当然我们也是可以自己通过 Mutex 实现 sync.Once 的功能,但是相比来说繁琐了那么一点, 因为我们不仅要自己去控制锁,还要通过一个标识来标志是否已经执行过。

Once 的实现

Once 的实现非常简单,如下,就只有 20 来行代码,但里面包含了 go 并发、同步的一些常见处理方法。

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

简要说明:

  • done 字段指示了操作是否已执行,也就是我们传递给 Do 的函数是否已经被执行。
  • Do 方法接收一个函数参数,这个函数参数只会被执行一次。
  • Once 内部是通过 Mutex 来实现不同协程之间的同步的。

使用示例

在下面的例子中,once.Do(test) 被执行了 3 次,但是最终 test 只被执行了一次。

package sync
import (
   "fmt"
   "sync"
   "testing"
)
var once sync.Once
var a = 0
func test() {
   a++
}
func TestOnce(t *testing.T) {
   var wg sync.WaitGroup
   wg.Add(3)
   for i := 0; i < 3; i++ {
      go func() {
         // once.Do 会调用 3 次,但最终只会执行一次
         once.Do(test)
         wg.Done()
      }()
   }
   wg.Wait()
   fmt.Println(a) // 1
}

Once 的一些工作机制

  • OnceDo 方法可以保证,在多个 goroutine 同时执行 Do 方法的时候, 在第一个抢占到 Do 执行权的 goroutine 执行返回之前,其他 goroutine 都会阻塞在 Once.Do 的调用上, 只有第一个 Do 调用返回的时候,其他 goroutine 才可以继续执行下去,并且其他所有的 goroutine 不会再执行传递给 Do 的函数。(如果是初始化的场景,这可以避免尚未初始化完成就执行其他的操作)
  • 如果 Once.Do 发生 panic 的时候,传递给 Do 的函数依然被标记为已完成。后续对 Do 的调用也不会再执行传给 Do 的函数参数。
  • 我们不能简单地通过 atomic.CompareAndSwapUint32 来决定是否执行 f(),因为在多个 goroutine 同时执行的时候,它无法保证 f() 只被执行一次。所以 Once 里面用了 Mutex,这样就可以有效地保护临界区。
// 错误实现,这不能保证 f 只被执行一次
if atomic.CompareAndSwapUint32(&amp;o.done, 0, 1) {
    f()
}
  • Once.Do 的函数参数是没有参数的,如果我们需要传递一些参数,可以再对 f 做一层包裹。
config.once.Do(func() { config.init(filename) })

Once 详解

hotpath

这里说的 hotpath 指的是 Once 里的第一个字段 done

type Once struct {
   // hotpath
   done uint32
   m    Mutex
}

Once 结构体的第一个字段是 done,这是因为 done 的访问是远远大于 Once 中另外一个字段 m 的, 放在第一个字段中,编译器就可以做一些优化,因为结构体的地址其实就是结构体第一个字段的地址, 这样一来,在访问 done 字段的时候,就不需要通过结构体地址 + 偏移量的方式来访问, 这在一定程度上提高了性能。

结构体地址计算示例:

type person struct {
   name string
   age  int
}
func TestStruct(t *testing.T) {
   var p = person{
      name: "foo",
      age:  10,
   }
   // p 和 p.name 的地址相同
   // 0xc0000100a8, 0xc0000100a8
   fmt.Printf("%p, %p\n", &p, &p.name)
   // p.age 的地址
   // 0xc0000100b8
   fmt.Printf("%p\n", &p.age)
   // p.age 的地址也可以通过:结构体地址 + age 字段偏移量 计算得出。
   // 0xc0000100b8
   fmt.Println(unsafe.Add(unsafe.Pointer(&p), unsafe.Offsetof(p.age)))
}

atomic.LoadUint32

func (o *Once) Do(f func()) {
   if atomic.LoadUint32(&o.done) == 0 {
      o.doSlow(f)
   }
}

Do 方法中,是通过 atomic.LoadUint32 的方式来判断 done 是否等于 0 的, 这是因为,如果直接使用 done == 0 的方式的话,就有可能导致在 doSlow 里面对 done 设置为 1 之后, 在 Do 方法里面无法正常观测到。因此用了 atomic.LoadUint32

而在 doSlow 里面是可以通过 done == 0 来判断的,这是因为 doSlow 里面已经通过 Mutex 保护起来了。 唯一设置 done = 1 的地方就在临界区里面,所以 doSlow 里面通过 done == 0 来判断是完全没有问题的。

atomic.StoreUint32

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

doSlow 方法中,设置 done 为 1 也是通过 atomic.StoreUint32 来设置的。 这样就可以保证在设置了 done 为 1 之后,可以及时被其他 goroutine 看到。

Mutex

doSlow 的实现里面,最终还是要通过 Mutex 来保护临界区, 通过 Mutex 可以实现 f 只被执行一次,并且其他的 goroutine 都可以使用这一次 f 的执行结果。 因为其他 goroutine 在第一次 f 调用未返回之前,都阻塞在获取 Mutex 锁的地方, 当它们获取到 Mutex 锁的时候,得以继续往下执行,但这个时候 f 已经执行完毕了, 所以当它们获取到 Mutex 锁之后其实什么也没有干。

但是它们的阻塞状态被解除了,可以继续往下执行。

总结

  • Once 保证了传入的函数只会执行一次,这常常用在一些初始化的场景、或者单例模式。
  • Once 可以保证所有对 Do 的并发调用都是安全的,所有对 Once.Do 调用之后的操作,一定会在第一次对 f 调用之后执行。(没有获取到 f 执行权的 goroutine 会阻塞)
  • 即使 Once.Do 里面的 f 出现了 panic,后续也不会再次调用 f

以上就是go sync Once实现原理示例解析的详细内容,更多关于go sync Once实现原理的资料请关注我们其它相关文章!

(0)

相关推荐

  • Go语言并发编程 sync.Once

    sync.Once用于保证某个动作只被执行一次,可用于单例模式中,比如初始化配置.我们知道init()函数也只会执行一次,不过它是在main()函数之前执行,如果想要在代码执行过程中只运行某个动作一次,可以使用sync.Once,下面来介绍一下它的使用方法. 先来看下面的代码: package main import ( "fmt" "sync" ) func main() { var num = 6 var once sync.Once add_one := fu

  • GoLang中的sync包Once使用执行示例

    目录 背景 One简介 示例 注意 源码解读 背景 在系统初始化的时候,某些代码只想被执行一次,这时应该怎么做呢,没有学习 Once 前,大家可能想到 声明一个标识,表示是否初始化过,然后初始化这个标识加锁,更新这个标识. 但是学会了 One 的使用可以更加简单的解决这个问题 One简介 Once 包主要用于在并发执行代码的时候,某部分代码只会被执行 一次. Once 的使用也非常简单,Once 只有一个 Do 方法,接收一个无参数无返回值的函数类型的参数 f,不管调用多少次 Do 方法,参数

  • GoLang的sync.WaitGroup与sync.Once简单使用讲解

    目录 一.sync.WaitGroup的简单实用 Add方法 Done方法 Wait方法 二.sync.WaitGroup类型值中计数器的值可以小于0吗 三.sync.Once 用法 sync.Once类型中的uint32类型的字段 Do方法的功能特点 一.sync.WaitGroup的简单实用 在之前,我们使用通道,来主goroutine中等待其他goroutine执行完成: func coordinateWithChan() { sign := make(chan struct{}, 2)

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

    目录 一.序 二. 源码分析 2.1结构体 2.2 接口 三. 使用场景案例 3.1 单例模式 3.2 加载配置文件示例 四.总结 五. 参考 一.序 单从库名大概就能猜出其作用.sync.Once使用起来很简单, 下面是一个简单的使用案例 package main import ( "fmt" "sync" ) func main() { var ( once sync.Once wg sync.WaitGroup ) for i := 0; i < 10;

  • Golang基于sync.Once实现单例的操作代码

    目录 基于sync.Once实现单例 单例类型定义Driver 类Field conn once.Do(func() {}) 并发访问once.Do() 对外暴露方法Conn() 重新new(Driver)会发生什么? 在go里实现单例模式有多种方式: 基于lock 基于init函数 基于sync.Once 本文介绍基于sync.Once的方式来实现单例,熟练掌握这种模式,并理解其底层原理,对大部分人来讲已经完全够用了. 基于sync.Once实现单例 // 其他package也可见,在其他地方

  • 一文解析 Golang sync.Once 用法及原理

    目录 前言 1. 定位 2. 对外接口 3. 实战用法 3.1 初始化 3.2 单例模式 3.3 关闭channel 4. 原理 5. 避坑 前言 在此前一篇文章中我们了解了 Golang Mutex 原理解析,今天来看一个官方给出的 Mutex 应用场景:sync.Once. 1. 定位 Once is an object that will perform exactly one action. sync.Once 是 Go 标准库提供的使函数只执行一次的实现,常应用于单例模式,例如初始化配

  • go sync Once实现原理示例解析

    目录 正文 Once 的实现 使用示例 Once 的一些工作机制 Once 详解 hotpath atomic.LoadUint32 atomic.StoreUint32 Mutex 总结 正文 在很多情况下,我们可能需要控制某一段代码只执行一次,比如做某些初始化操作,如初始化数据库连接等. 对于这种场景,go 为我们提供了 sync.Once 对象,它保证了某个动作只被执行一次. 当然我们也是可以自己通过 Mutex 实现 sync.Once 的功能,但是相比来说繁琐了那么一点, 因为我们不仅

  • Kotlin协程之Flow基础原理示例解析

    目录 引言 一.Flow的创建 二.Flow的消费 1.SafeFlow类 2.AbstractFlow类 3. SafeCollector类 4.消费过程中的挂起 引言 本文分析示例代码如下: launch(Dispatchers.Main) { flow { emit(1) emit(2) }.collect { delay(1000) withContext(Dispatchers.IO) { Log.d("liduo", "$it") } Log.d(&qu

  • go语言csrf库使用实现原理示例解析

    目录 引言 csrf小档案 一.CSRF及其实现原理 CSRF攻击示例 二.如何预防 三.CSRF包的使用及实现原理 csrf包的安装 基本使用 使用net/http包启动的服务 echo框架下使用csrf包 gin框架下使用csrf包 beego框架下使用csrf包 实现原理 csrf结构体 csrf包的工作流程 为什么GET.HEAD.OPTIONS.TRACE的请求方法不需要token验证 总结 引言 今天给大家推荐的是web应用安全防护方面的一个包:csrf.该包为Go web应用中常见

  • react fiber执行原理示例解析

    目录 为什么要使用fiber,要解决什么问题? fiber是什么? 数据结构 执行单元 浏览器工作: Fiber执行原理 workInProgress tree: currentFiber tree: Effects list: render阶段: 遍历节点过程: 收集effect list: commit阶段: 为什么commit必须是同步的操作的? 为什么要使用fiber,要解决什么问题? 在 react16 引入 Fiber 架构之前,react 会采用递归方法对比两颗虚拟DOM树,找出需

  • useReducer createContext代替Redux原理示例解析

    目录 前言 采用react-redux实现 采用react hooks模拟redux实现 异步action 总结 前言 最近看到很多采用useReducer + createContext 实现一个简易的redux的方案,今天亲自试了一下,发现还是会有一些区别的. 采用react-redux实现 这里使用react-redux 实现一个简单的状态管理例子. App.jsx根组件 import React from 'react'; import { Button } from './Button

  • flutter Bloc 实现原理示例解析

    目录 序言 1. 事件流 > 状态流 (中转) 2. 使用 BlocBuilder 实时监听状态变更, 如何实现的呢? 总结 扩展 序言 在flutter开发中,我们使用 bloc 框架,基于状态变更进行响应式开发.本篇文章,小轰将 bloc 核心业务块进行拆解简化,聊一聊它的实现思想,bloc 核心能力分为如下两点: 添加事件 event,将 '事件流' 转换为 '状态流' state 监听 bloc 流,每次 state 状态变更,通知 widget 更新 下面,用自定义Bloc的方式,来给

  • java LockSupport实现原理示例解析

    目录 引言 LockSupport常见函数 LockSupport.park LockSupport.unpark 引言 前文中了解到AQS借助LockSupport.park和LockSupport.unpark完成线程的阻塞和唤醒,那么LockSupport内部又是怎么实现的?这是一个什么类? LockSupport是用于使用锁阻塞线程的基础实现,是其他同步类的基础,这个类为每个使用它的线程关联一个许可证(有点类似于Semaphore),如果许可证可用,线程调用park方法时会立即返回,线程

  • 网页资源阻塞浏览器加载的原理示例解析

    目录 正文 测试前环境准备 图片会造成阻塞吗? CSS 加载阻塞 CSS 会阻塞后面 JS 的执行吗? JS 加载阻塞 defer 和 async 动态脚本会造成阻塞吗? DOMContentLoaded 和 onload DOMContentLoaded 遇到脚本 DOMContentLoaded 遇到样式 正文 一个页面允许加载的外部资源有很多,常见的有脚本.样式.字体.图片和视频等,对于这些外部资源究竟是如何影响整个页面的加载和渲染的呢?今天来一探究竟. 如何用 Chrome 定制网络加载

  • OPENMP SECTIONS CONSTRUCT原理示例解析

    目录 前言 编译器角度分析 动态库函数分析 总结 前言 在本篇文章当中主要给大家介绍 OpenMP 当中主要给大家介绍 OpenMP 当中 sections construct 的实现原理以及他调用的动态库函数分析.如果已经了解过了前面的关于 for 的调度方式的分析,本篇文章就非常简单了. 编译器角度分析 在这一小节当中我们将从编译器角度去分析编译器会怎么处理 sections construct ,我们以下面的 sections construct 为例子,看看编译器是如何处理 sectio

  • open 打开浏览器的过程原理示例解析

    目录 前言 使用 open open 的实现原理 总结 前言 启动项目时,在本地服务器启动后会自动帮我们打开浏览器,程序是如何做到呢?又是哪些代码在起作用呢?希望通过本章节的学习,达成如下目标: 学习程序自动打开浏览的原理 学会使用 Node.js 强大的 child_process 模块 源码地址:sindresorhus/open npm: open - npm (npmjs.com) 使用 配置 webpack 的 devServer 选项: module.exports = { //..

随机推荐