go并发利器sync.Once使用示例详解

目录
  • 1. 简介
  • 2. 基本使用
    • 2.1 基本定义
    • 2.2 使用方式
    • 2.3 使用例子
  • 3. 原理
  • 4. 使用注意事项
    • 4.1 不能将sync.Once作为函数局部变量
    • 4.2 不能在once.Do中再次调用once.Do
    • 4.3 需要对传入的函数进行错误处理
      • 4.3.1 基本说明
      • 4.3.2 未错误处理导致的问题
      • 4.3.3 处理方式
  • 5. 总结

1. 简介

本文主要介绍 Go 语言中的 Once 并发原语,包括 Once 的基本使用方法、原理和注意事项,从而对 Once 的使用有基本的了解。

2. 基本使用

2.1 基本定义

sync.Once是Go语言中的一个并发原语,用于保证某个函数只被执行一次。Once类型有一个Do方法,该方法接收一个函数作为参数,并在第一次调用时执行该函数。如果Do方法被多次调用,只有第一次调用会执行传入的函数。

2.2 使用方式

使用sync.Once非常简单,只需要创建一个Once类型的变量,然后在需要保证函数只被执行一次的地方调用其Do方法即可。下面是一个简单的例子:

var once sync.Once
func initOperation() {
    // 这里执行一些初始化操作,只会被执行一次
}
func main() {
    // 在程序启动时执行initOperation函数,保证初始化只被执行一次
    once.Do(initOperation)
    // 后续代码
}

2.3 使用例子

下面是一个简单使用sync.Once的例子,其中我们使用sync.Once来保证全局变量config只会被初始化一次:

package main
import (
    "fmt"
    "sync"
)
var (
    config map[string]string
    once   sync.Once
)
func loadConfig() {
    // 模拟从配置文件中加载配置信息
    fmt.Println("load config...")
    config = make(map[string]string)
    config["host"] = "127.0.0.1"
    config["port"] = "8080"
}
func GetConfig() map[string]string {
    once.Do(loadConfig)
    return config
}
func main() {
    // 第一次调用GetConfig会执行loadConfig函数,初始化config变量
    fmt.Println(GetConfig())
    // 第二次调用GetConfig不会执行loadConfig函数,直接返回已初始化的config变量
    fmt.Println(GetConfig())
}

在这个例子中,我们定义了一个全局变量config和一个sync.Once类型的变量once。在GetConfig函数中,我们通过调用once.Do方法来保证loadConfig函数只会被执行一次,从而保证config变量只会被初始化一次。 运行上面的程序,输出如下:

load config...
map[host:127.0.0.1 port:8080]
map[host:127.0.0.1 port:8080]

可以看到,GetConfig函数在第一次调用时执行了loadConfig函数,初始化了config变量。在第二次调用时,loadConfig函数不会被执行,直接返回已经初始化的config变量。

3. 原理

下面是sync.Once的具体实现如下:

type Once struct {
   done uint32
   m    Mutex
}
func (o *Once) Do(f func()) {
    // 判断done标记位是否为0
   if atomic.LoadUint32(&o.done) == 0 {
      // Outlined slow-path to allow inlining of the fast-path.
      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()
   }
}

sync.Once的实现原理比较简单,主要依赖于一个done标志位和一个互斥锁。当Do方法被第一次调用时,会先原子地读取done标志位,如果该标志位为0,说明函数还没有被执行过,此时会加锁并执行传入的函数,并将done标志位置为1,然后释放锁。如果标志位为1,说明函数已经被执行过了,直接返回。

4. 使用注意事项

4.1 不能将sync.Once作为函数局部变量

下面是一个简单的例子,说明将 sync.Once 作为局部变量会导致的问题:

var config map[string]string
func initConfig() {
    fmt.Println("initConfig called")
    config["1"] = "hello world"
}
func getConfig() map[string]string{
    var once sync.Once
    once.Do(initCount)
    fmt.Println("getConfig called")
}
func main() {
    for i := 0; i < 10; i++ {
        go getConfig()
    }
    time.Sleep(time.Second)
}

这里初始化函数会被多次调用,这与initConfig 方法只会执行一次的预期不符。这是因为将 sync.Once 作为局部变量时,每次调用函数都会创建新的 sync.Once 实例,每个 sync.Once 实例都有自己的 done 标志,多个实例之间无法共享状态。导致初始化函数会被多次调用。

如果将 sync.Once 作为全局变量或包级别变量,就可以避免这个问题。所以基于此,不能定义sync.Once 作为函数局部变量来使用。

4.2 不能在once.Do中再次调用once.Do

下面举一个在once.Do方法中再次调用once.Do 方法的例子:

package main
import (
"fmt"
"sync"
)
func main() {
   var once sync.Once
   var onceBody func()
   onceBody = func() {
      fmt.Println("Only once")
      once.Do(onceBody) // 再次调用once.Do方法
   }
   // 执行once.Do方法
   once.Do(onceBody)
   fmt.Println("done")
}

在上述代码中,当once.Do(onceBody)第一次执行时,会输出"Only once",然后在执行once.Do(onceBody)时会发生死锁,程序无法继续执行下去。

这是因为once.Do()方法在执行过程中会获取互斥锁,在方法内再次调用once.Do()方法,那么就会在获取互斥锁时出现死锁。

因此,我们不能在once.Do方法中再次调用once.Do方法。

4.3 需要对传入的函数进行错误处理

4.3.1 基本说明

一般情况下,如果传入的函数不会出现错误,可以不进行错误处理。但是,如果传入的函数可能出现错误,就必须对其进行错误处理,否则可能会导致程序崩溃或出现不可预料的错误。

因此,在编写传入Once的Do方法的函数时,需要考虑到错误处理问题,保证程序的健壮性和稳定性。

4.3.2 未错误处理导致的问题

下面举一个传入的函数可能出现错误,但是没有对其进行错误处理的例子:

import (
   "fmt"
   "net"
   "sync"
)
var (
   initialized bool
   connection  net.Conn
   initOnce    sync.Once
)
func initConnection() {
   connection, _ = net.Dial("tcp", "err_address")
}
func getConnection() net.Conn {
   initOnce.Do(initConnection)
   return connection
}
func main() {
   conn := getConnection()
   fmt.Println(conn)
   conn.Close()
}

在上面例子中,其中initConnection 为传入的函数,用于建立TCP网络连接,但是在sync.Once中执行该函数时,是有可能返回错误的,而这里并没有进行错误处理,直接忽略掉错误。此时调用getConnection 方法,如果initConnection报错的话,获取连接时会返回空连接,后续调用将会出现空指针异常。因此,如果传入sync.Once当中的函数可能发生异常,此时应该需要对其进行处理。

4.3.3 处理方式

  • 4.3.3.1 panic退出执行

应用程序第一次启动时,此时调用sync.Once来初始化一些资源,此时发生错误,同时初始化的资源是必须初始化的,可以考虑在出现错误的情况下,使用panic将程序退出,避免程序继续执行导致更大的问题。具体代码示例如下:

import (
   "fmt"
   "net"
   "sync"
)
var (
   connection  net.Conn
   initOnce    sync.Once
)
func initConnection() {
   // 尝试建立连接
   connection, err = net.Dial("tcp", "err_address")
    if err != nil {
       panic("net.Dial error")
    }
}
func getConnection() net.Conn {
   initOnce.Do(initConnection)
   return connection
}

如上,当initConnection方法报错后,此时我们直接panic,退出整个程序的执行。

  • 4.3.3.2 修改sync.Once实现,Do函数的语意修改为只成功执行一次

在程序运行过程中,可以选择记录下日志或者返回错误码,而不需要中断程序的执行。然后下次调用时再执行初始化的逻辑。这里需要对sync.Once进行改造,原本sync.Once中Do函数的实现为执行一次,这里将其修改为只成功执行一次。具体使用方式需要根据具体业务场景来决定。下面是其中一个实现:

type MyOnce struct {
   done int32
   m    sync.Mutex
}
func (o *MyOnce) Do(f func() error) {
   if atomic.LoadInt32(&o.done) == 0 {
      o.doSlow(f)
   }
}
func (o *MyOnce) doSlow(f func() error) {
   o.m.Lock()
   defer o.m.Unlock()
   if o.done == 0 {
      // 只有在函数调用不返回err时,才会设置done
      if err := f(); err == nil {
         atomic.StoreInt32(&o.done, 1)
      }
   }
}

上述代码中,增加了一个错误处理逻辑。当 f() 函数返回错误时,不会将 done 标记位置为 1,以便下次调用时可以重新执行初始化逻辑。

需要注意的是,这种方式虽然可以解决初始化失败后的问题,但可能会导致初始化函数被多次调用。因此,在编写f() 函数时,需要考虑到这个问题,以避免出现不可预期的结果。

下面是一个简单的例子,使用我们重新实现的Once,展示第一次初始化失败时,第二次调用会重新执行初始化逻辑,并成功初始化:

var (
   hasCall bool
   conn    net.Conn
   m       MyOnce
)
func initConn() (net.Conn, error) {
   fmt.Println("initConn...")
   // 第一次执行,直接返回错误
   if !hasCall {
      return nil, errors.New("init error")
   }
   // 第二次执行,初始化成功,这里默认其成功
   conn, _ = net.Dial("tcp", "baidu.com:80")
   return conn, nil
}
func GetConn() (net.Conn, error) {
   m.Do(func() error {
      var err error
      conn, err = initConn()
      if err != nil {
         return err
      }
      return nil
   })
   // 第一次执行之后,将hasCall设置为true,让其执行初始化逻辑
   hasCall = true
   return conn, nil
}
func main() {
   // 第一次执行初始化逻辑,失败
   GetConn()
   // 第二次执行初始化逻辑,还是会执行,此次执行成功
   GetConn()
   // 第二次执行成功,第三次调用,将不会执行初始化逻辑
   GetConn()
}

在这个例子中,第一次调用Do方法初始化失败了,done标记位被设置为0。在第二次调用Do方法时,由于done标记位为0,会重新执行初始化逻辑,这次初始化成功了,done标记位被设置为1。第三次调用,由于之前Do方法已经执行成功了,不会再执行初始化逻辑。

5. 总结

本文旨在介绍Go语言中的Once并发原语,包括其基本使用、原理和注意事项,让大家对Once有一个基本的了解。

首先,我们通过示例演示了Once的基本使用方法,并强调了其仅会执行一次的特性。然后,我们解释了Once仅执行一次的原因,使读者更好地理解Once的工作原理。最后,我们指出了使用Once时的一些注意事项,以避免误用。

总之,本文全面地介绍了Go语言中的Once并发原语,使读者能够更好地理解和应用它。

以上就是go并发利器sync.Once使用示例详解的详细内容,更多关于go并发利器sync.Once的资料请关注我们其它相关文章!

(0)

相关推荐

  • go打包aar及flutter调用aar流程详解

    目录 一.目的 二.背景 三.流程 问题: 问题一:go如何打包为移动端的包 1.环境配置 2.go配置与打包 问题二:flutter如何调用aar 第一步:存放aar与修改gradle配置 第二步:修改MainActivity.java入口代码 第三步:flutter调用 四.结论 一.目的 本篇文章的目的是记录本人使用flutter加载与调用第三方aar包. 二.背景 本人go后端,业余时间喜欢玩玩flutter.一直有一个想法,go可以编译为第三方平台的可执行程序,而flutter可以是一

  • Go CSV包实现结构体和csv内容互转工具详解

    目录 引言 gocsv小档案 gocsv的基本功能 gocsv.UnmarshalFile函数:csv内容转成结构体 gocsv.MarshalFile函数:结构体转成csv文件 自定义类型转换器 自定义CSV的Reader/Writer gocsv包的特点总结 引言 大家在开发中一定遇到过将数据导出成csv格式文件的需求.go标准库中的csv包是只能写入字符串类型的切片.而在go中一般都是将内容写入到结构体中.所以,若使用标准的csv包,就需要将结构体先转换成对应的字符串类型,再写入文件.那可

  • 深入浅出Golang中的sync.Pool

    目录 一.原理分析 1.1 结构依赖关系图 1.2 用图让代码说话 1.3 Put过程分析 二.学习收获 2.1 如何自己实现一个无锁队列 学习到的内容: 1.一个64位的int类型值,充分利用高32位和低32位,进行相关加减以及从一个64位中拆出高32位和低32位. 扩展:如何自己实现一个无锁队列. 如何判断队列是否满. 如何实现无锁化. 优化方面需要思考的东西. 2.内存相关操作以及优化 内存对齐 CPU Cache Line 直接操作内存. 一.原理分析 1.1 结构依赖关系图 下面是相关

  • go sync.Once实现高效单例模式详解

    目录 1. 简介 2. 基本实现 2.1 单例模式定义 2.2 sync.Once实现单例模式 2.3 其他方式实现单例模式 2.3.1 全局变量定义时赋值,实现单例模式 2.3.2 init 函数实现单例模式 2.3.3 使用互斥锁实现单例模式 2.4 使用sync.Once实现单例模式的优点 2.5 sync.Once和init方法适用场景 3. gin中单例模式的使用 3.1 背景 3.2 具体实现 3.3 sync.Once实现单例的好处 4.总结 1. 简介 本文介绍使用sync.On

  • Golang使用ChatGPT生成单元测试实践

    目录 前言 Part1 easy:单个函数,无复杂依赖 Part2 normal :里面有一些外部import Part3 hard:对外部repo进行mock(gomock举例) 一些痛点 其他用法 前言 目前gpt本质上是续写,所以在待测函数函数定义清晰的情况下,单元测试可以适当依赖它进行生成. 收益是什么: 辅助生成测试用例&测试代码,降低单元测试编写的心智成本 辅助code review,帮助发现代码显式/潜在问题 本文测试环境: gpt: gpt-3.5-turbo go:go 1.1

  • Go interface接口声明实现及作用详解

    目录 什么是接口 接口的定义与作用 接口的声明和实现 接口的声明 接口的实现 接口类型断言 空接口 接口实际用途 通过接口实现面向对象多态特性 通过接口实现一个简单的 IoC (Inversion of Control) 什么是接口 接口是一种定义规范,规定了对象应该具有哪些方法,但并不指定这些方法的具体实现.在 Go 语言中,接口是由一组方法签名(方法名.参数类型.返回值类型)定义的.任何实现了这组方法的类型都可以被认为是实现了这个接口. 这种方式使得接口能够描述任意类型的行为,而不用关心其实

  • go并发利器sync.Once使用示例详解

    目录 1. 简介 2. 基本使用 2.1 基本定义 2.2 使用方式 2.3 使用例子 3. 原理 4. 使用注意事项 4.1 不能将sync.Once作为函数局部变量 4.2 不能在once.Do中再次调用once.Do 4.3 需要对传入的函数进行错误处理 4.3.1 基本说明 4.3.2 未错误处理导致的问题 4.3.3 处理方式 5. 总结 1. 简介 本文主要介绍 Go 语言中的 Once 并发原语,包括 Once 的基本使用方法.原理和注意事项,从而对 Once 的使用有基本的了解.

  • Go编程库Sync.Pool用法示例详解

    目录 场景 用法 创建 GET & PUT 优化 Log 函数 性能测试 场景 go 如果频繁地创建.销毁对象(比如 http 服务的 json 对象,日志内容等),会对 GC 造成压力.比如下面的 Log 函数,在高并发情况下,需要频繁地创建和销毁 buffer. func Log(w io.Writer, key, val string) { b := new(bytes.Buffer) // 按一定的格式打印日志,这一段不是重点 b.WriteString(time.Now().UTC()

  • vue 之 .sync 修饰符示例详解

    在一些情况下,我们可能会需要对一个 prop (父子组件传递数据的属性) 进行"双向绑定". 在vue 1.x 中的 .sync 修饰符所提供的功能.当一个子组件改变了一个带 .sync 的prop的值时,这个变化也会同步到父组件中所绑定的值. 这很方便,但也会导致问题,因为它破坏了单向数据流.(数据自上而下流,事件自下而上走) 由于子组件改变 prop 的代码和普通的状体改动代码毫无区别,所以当你光看子组件的代码时,你完全不知道它合适悄悄地改变了父组件的状态. 这在 debug 复杂

  • GO中sync包自由控制并发示例详解

    目录 资源竞争 sync.Mutex sync.RWMutex sync.WaitGroup sync.Once sync.Cond 资源竞争 channel 常用于并发通信,要保证并发安全,主要使用互斥锁.在并发的过程中,当一个内存被多个 goroutine 同时访问时,就会产生资源竞争的情况.这块内存也可以称为共享资源. 并发时对于共享资源必然会出现抢占资源的情况,如果是对某资源的统计,很可能就会导致结果错误.为保证只有一个协程拿到资源并操作它,可以引入互斥锁 sync.Mutex. syn

  • Go语言通过WaitGroup实现控制并发的示例详解

    目录 与Channel区别 基本使用示例 完整代码 特别提示 多任务示例 完整代码 与Channel区别 Channel能够很好的帮助我们控制并发,但是在开发习惯上与显示的表达不太相同,所以在Go语言中可以利用sync包中的WaitGroup实现并发控制,更加直观. 基本使用示例 我们将之前的示例加以改造,引入sync.WaitGroup来实现并发控制. 首先我们在主函数中定义WaitGroup var wg sync.WaitGroup 每执行一个任务,则调用Add()方法 wg.Add(1)

  • Golang WorkerPool线程池并发模式示例详解

    目录 正文 处理CVS文件记录 获取测试数据 线程池耗时差异 正文 Worker Pools 线程池是一种并发模式.该模式中维护了固定数量的多个工作器,这些工作器等待着管理者分配可并发执行的任务.该模式避免了短时间任务创建和销毁线程的代价. 在 golang 中,我们使用 goroutine 和 channel 来构建这种模式.工作器 worker 由一个 goroutine 定义,该 goroutine 通过 channel 获取数据. 处理CVS文件记录 接下来让我们通过一个例子,来进一步理

  • Go单元测试利器testify使用示例详解

    目录 testify assert 包 require 包 mock 包 suite 包 testify 在团队里推行单元测试的时候,有一个反对的意见是:写单元测试耗时太多.且不论这个意见对错,单元测试确实不应该太费时间.这时候,一个好的单测辅助工具,显得格外重要.本文推荐的 testify(github.com/stretchr/te…) 包,具有断言.mock 等功能,能配合标准库,使你的单元测试更加简洁易读. testify 有三个主要功能: 断言,在 assert 包和 require

  • TDesign在vitest的实践示例详解

    目录 起源 痛点与现状 vitest 迁移 配置文件改造 开发环境 集成测试 ssr 环境 csr 环境 配置文件 兼容性 结果 CI测试速度提升 更清爽的日志信息 起源 在 tdesign-vue-next 的 CI 流程中,单元测试模块的执行效率太低,每次在单元测试这个环节都需要花费 6m 以上.加上依赖按照,lint 检查等环节,需要花费 8m 以上. 加上之前在单元测试这一块只是简单的处理了一下,对开发者提交的组件也没有相应的要求,只是让它能跑起来就好.另一方面单元测试目前是 TD 发布

  • iOS开发探索多线程GCD队列示例详解

    目录 引言 进程与线程 1.进程的定义 2.线程的定义 3. 进程和线程的关系 4. 多线程 5. 时间片 6. 线程池 GCD 1.任务 2.队列 3.死锁 总结 引言 在iOS开发过程中,绕不开网络请求.下载图片之类的耗时操作,这些操作放在主线程中处理会造成卡顿现象,所以我们都是放在子线程进行处理,处理完成后再返回到主线程进行展示. 多线程贯穿了我们整个的开发过程,iOS的多线程操作有NSThread.GCD.NSOperation,其中我们最常用的就是GCD. 进程与线程 在了解GCD之前

  • Golang信号量设计实现示例详解

    目录 开篇 信号量 semaphore 扩展库实现 Acquire Release TryAcquire 总结 开篇 在我们此前的文章 Golang Mutex 原理解析 中曾提到过,Mutex 的底层结构包含了两个字段,state 和 sema: type Mutex struct { state int32 sema uint32 } state 代表互斥锁的状态,比如是否被锁定: sema 表示信号量,协程阻塞会等待该信号量,解锁的协程释放信号量从而唤醒等待信号量的协程. 这个 sema

随机推荐