golang中sync.Once只执行一次的原理解析

 更新时间:2023年09月11日 10:30:29   作者:写代码的lorre  
在某些场景下,我们希望某个操作或者函数仅被执行一次,比如单例模式的初始化,一些资源配置的加载等,golang中的sync.Once就实现了这个功能,本文就和大家一起解析sync.Once只执行一次的原理,需要的朋友可以参考下

背景

在某些场景下,我们希望某个操作或者函数仅被执行一次,比如单例模式的初始化,一些资源配置的加载等。

golang中的sync.Once就实现了这个功能,Once只对外提供一个Do方法,Do方法只接收一个函数参数,它可以保证并发场景下,多次对Do方法进行调用时,参数对应的函数只被执行一次

快速入门

定义一个f1函数,同时开启10个并发,通过Once提供的Do方法去执行f1函数,Once可以保证f1函数只被执行一次

func TestOnce(t *testing.T) {
   once := sync.Once{}
   f1 := func() {
      fmt.Println("f1 func")
   }
   wg := sync.WaitGroup{}
   for i := 0; i < 10; i++ {
      wg.Add(1)
      go func() {
         defer wg.Done()
         once.Do(f1)
      }()
   }
   wg.Wait()
}

源码分析

golang版本:1.18.2

源码路径:src/sync/Once.go

// Once is an object that will perform exactly one action.
//
// A Once must not be copied after first use.
type Once struct {
   // done indicates whether the action has been performed.
   // It is first in the struct because it is used in the hot path.
   // The hot path is inlined at every call site.
   // Placing done first allows more compact instructions on some architectures (amd64/386),
   // and fewer instructions (to calculate offset) on other architectures.
   done uint32
   m    Mutex
}
// Once只对外提供一个Do方法
func (o *Once) Do(f func()) {}
  • Once内部有两个字段:done和m
  • done用来表示传入的函数是否已执行完成,未执行和执行中时,done=0,执行完成时,done=1
  • m互斥锁,用来保证并发调用时,传入的函数只被执行一次

Do()

// Do calls the function f if and only if Do is being called for the
// first time for this instance of Once. In other words, given
//     var once Once
// if once.Do(f) is called multiple times, only the first call will invoke f,
// even if f has a different value in each invocation. A new instance of
// Once is required for each function to execute.
//
// Do is intended for initialization that must be run exactly once. Since f
// is niladic, it may be necessary to use a function literal to capture the
// arguments to a function to be invoked by Do:
//     config.once.Do(func() { config.init(filename) })
//
// Because no call to Do returns until the one call to f returns, if f causes
// Do to be called, it will deadlock.
//
// If f panics, Do considers it to have returned; future calls of Do return
// without calling f.
//
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)
   }
}
func (o *Once) doSlow(f func()) {
   o.m.Lock()
   defer o.m.Unlock()
   if o.done == 0 {
      defer atomic.StoreUint32(&o.done, 1)
      f()
   }
}
  • 先通过atomic.LoadUint32(&o.done) == 0快速判断,传入的函数参数,是否已经执行完成。若done=0,表示函数未执行或正在执行中;若done=1,表示函数已执行完成,则快速返回
  • 通过m互斥锁进行加锁,保证并发安全
  • 通过o.done == 0二次确认,传入的函数参数是否已经被执行。若此时done=0,因为上一步已经通过m进行了加锁,所以可以保证的是,传入的函数还没有被执行,此时执行函数后,把done改为1即可;若此时done!=0,则表示在等待锁的期间,已经有其他goroutine成功执行了函数,此时直接返回即可

注意点一:同一个Once不能复用

func TestOnce(t *testing.T) {
   once := sync.Once{}
   f1 := func() {
      fmt.Println("f1 func")
   }
   f2 := func() {
      fmt.Println("f2 func")
   }
   // f1执行成功
   once.Do(f1)
   // f2不会执行
   once.Do(f2)
}

定义f1和f2两个函数,通过同一个Once来执行时,只能保证f1函数被执行一次

Once.Do保证的是第一个传入的函数参数只被执行一次,不是保证每一个传入的函数参数都只被执行一次,同一个Once不能复用,如果想要f1和f2都只被执行一次,可以初始化两个Once

注意点二:错误实现

if atomic.CompareAndSwapUint32(&o.done, 0, 1) {
   f()
}

为什么通过CAS来实现是错误的?

因为CAS只能保证函数被执行一次,但是不能保证f()还在执行时,其他goroutine等待其执行完成后再返回。这个很重要,当我们传入的函数是比较耗时的操作,比如和db建立连接等,就必须等待函数执行完成再返回,不然就会出现一些未知的操作

注意点三:atomic.LoadUint32(&o.done) == 0和atomic.StoreUint32(&o.done, 1)

为什么使用atomic.LoadUint32(&o.done) == 0来判断,而不是使用o.done == 0来判断

为了防止发生数据竞争,使用o.done == 0来判断,会发生数据竞争(Data Race)

数据竞争问题是指至少存在两个线程/协程去读写某个共享内存,其中至少一个线程/协程对其共享内存进行写操作

多个线程/协程同时对共享内存的进行写操作时,在写的过程中,其他的线程/协程读到数据是内存数据中非正确预期的

验证数据竞争问题:

package main
import (
   "fmt"
   "sync"
)
func main() {
   once := Once{}
   var wg sync.WaitGroup
   wg.Add(2)
   go func() {
      once.Do(print)
      wg.Done()
   }()
   go func() {
      once.Do(print)
      wg.Done()
   }()
   wg.Wait()
   fmt.Println("end")
}
func print() {
   fmt.Println("qqq")
}
type Once struct {
   done uint32
   m    sync.Mutex
}
func (o *Once) Do(f func()) {
   // 原来:atomic.LoadUint32(&o.done) == 0
   if o.done == 0 {
      o.doSlow(f)
   }
}
func (o *Once) doSlow(f func()) {
   o.m.Lock()
   defer o.m.Unlock()
   if o.done == 0 {
      // 原来:atomic.StoreUint32(&o.done, 1)
      defer func() {
         o.done = 1
      }()
      f()
   }
}

执行命令:

 go run -race main.go

执行结果:

qqq
==================
WARNING: DATA RACE
Write at 0x00c0000bc014 by goroutine 7:
  main.(*Once).doSlow.func1()
      /Users/cr/Documents/golang/src/ahut.com/go/demo/main.go:44 +0x32
  runtime.deferreturn()
      /usr/local/go/src/runtime/panic.go:436 +0x32
  main.(*Once).Do()
      /Users/cr/Documents/golang/src/ahut.com/go/demo/main.go:35 +0x52
  main.main.func1()
      /Users/cr/Documents/golang/src/ahut.com/go/demo/main.go:13 +0x37
Previous read at 0x00c0000bc014 by goroutine 8:
  main.(*Once).Do()
      /Users/cr/Documents/golang/src/ahut.com/go/demo/main.go:34 +0x3c
  main.main.func2()
      /Users/cr/Documents/golang/src/ahut.com/go/demo/main.go:17 +0x37
Goroutine 7 (running) created at:
  main.main()
      /Users/cr/Documents/golang/src/ahut.com/go/demo/main.go:12 +0x136
Goroutine 8 (running) created at:
  main.main()
      /Users/cr/Documents/golang/src/ahut.com/go/demo/main.go:16 +0x1da
==================
end
Found 1 data race(s)
exit status 66

以上就是golang中sync.Once只执行一次的原理解析的详细内容,更多关于golang sync.Once执行一次的资料请关注脚本之家其它相关文章!

相关文章

  • Go框架自动化工具Beego使用详解

    Go框架自动化工具Beego使用详解

    这篇文章主要为大家介绍了Go框架自动化工具Beego使用详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-12-12
  • Golang的循环语句和循环控制语句详解

    Golang的循环语句和循环控制语句详解

    循环语句为了简化程序中有规律的重复性操作,需要用到循环语句,和其他大多数编程语言一样,GO的循环语句有for循环,不同的是没有while循环,而循环控制语句可以改变循环语句的执行过程,下面给大家介绍下go循环语句和循环控制语句的相关知识,一起看看吧
    2021-11-11
  • Golang学习笔记(六):struct

    Golang学习笔记(六):struct

    这篇文章主要介绍了Golang学习笔记(六):struct,本文讲解了struct的声明及初始化、struct的匿名字段(继承)、method、method继承和重写等内容,需要的朋友可以参考下
    2015-05-05
  • RabbitMq如何做到消息的可靠性投递

    RabbitMq如何做到消息的可靠性投递

    现在的一些互联网项目或者是高并发的项目中很少有没有引入消息队列的。 引入消息队列可以给这个项目带来很多的好处,这篇文章主要为大家介绍了RabbitMq如何做到消息的可靠性投递,有需要的朋友可以借鉴参考下
    2022-12-12
  • GoLang unsafe包详细讲解

    GoLang unsafe包详细讲解

    从golang的定义来看,unsafe 是类型安全的操作。顾名思义,它应该非常谨慎地使用; unsafe可能很危险,但也可能非常有用。例如,当使用系统调用和Go结构必须具有与C结构相同的内存布局时,您可能别无选择,只能使用unsafe
    2022-10-10
  • Go单元测试利器testify使用示例详解

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

    这篇文章主要为大家介绍了Go单元测试利器testify使用示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-12-12
  • go语言实现二叉树的序例化与反序列化

    go语言实现二叉树的序例化与反序列化

    这篇文章主要介绍了go语言实现二叉树的序例化与反序列化,文章围绕主题展开详细内容介绍,具有一定的参考价值,需要的小伙伴可以参考一下
    2022-05-05
  • Golang中深拷贝与浅拷贝详解

    Golang中深拷贝与浅拷贝详解

    这篇文章主要为大家详细介绍一下Golang深拷贝和浅拷贝,文中有详细的代码示例供大家参考,需要的可以参考一下
    2023-05-05
  • Go语言中读取命令参数的几种方法总结

    Go语言中读取命令参数的几种方法总结

    部署golang项目时难免要通过命令行来设置一些参数,那么在golang中如何操作命令行参数呢?那么下面这篇文章就来给大家介绍了关于Go语言中读取命令参数的几种方法,文中通过示例代码介绍的非常详细,需要的朋友可以参考借鉴,下面随着小编来一起看看吧。
    2017-11-11
  • Go语言与其他语言进行交互的方式详解

    Go语言与其他语言进行交互的方式详解

    在当今的软件开发领域,多种编程语言常常需要协同工作,以充分利用各自的优势来构建复杂的应用系统,Go 语言作为一门高效、简洁的编程语言,也经常需要与其他语言进行交互,接下来,我们将详细探讨 Go 语言如何与其他语言进行交互,需要的朋友可以参考下
    2024-06-06

最新评论