原文连接:戳这里git
哈喽,你们好,我是asong
,这是我并发编程系列的第二篇文章. 上一篇咱们一块儿分析了atomic
包,今天咱们一块儿来看一看sync/once
的使用与实现.
sync.once
Go语言标准库中的sync.Once
能够保证go
程序在运行期间的某段代码只会执行一次,做用与init
相似,可是也有所不一样:github
init
函数是在文件包首次被加载的时候执行,且只执行一次。sync.Once
是在代码运行中须要的时候执行,且只执行一次。还记得我以前写的一篇关于go
单例模式,懒汉模式的一种实现就可使用sync.Once
,他能够解决双重检锁带来的每一次访问都要检查两次的问题,由于sync.once
的内部实现能够彻底解决这个问题(后面分析完源码就知道缘由了),下面咱们来看一看这种懒汉模式怎么写:面试
type singleton struct { } var instance *singleton var once sync.Once func GetInstance() *singleton { once.Do(func() { instance = new(singleton) }) return instance }
实现仍是比较简单,就不细说了。算法
sync.Once
的源码仍是不多的,首先咱们看一下他的结构:编程
// Once is an object that will perform exactly one action. 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/x86), // and fewer instructions (to calculate offset) on other architectures. done uint32 m Mutex }
只有两个字段,字段done
用来标识代码块是否执行过,字段m
是一个互斥锁。segmentfault
接下来咱们一块儿来看一下代码实现:设计模式
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() } }
这里把注释都省略了,反正都是英文,接下来咱用中文解释哈。sync.Once
结构对外只提供了一个Do()
方法,该方法的参数是一个入参为空的函数,这个函数也就是咱们想要执行一次的代码块。接下来咱们看一下代码流程:缓存
done
字段的值是否改变,没有改变则执行doSlow()
方法.doslow()
方法就开始执行加锁操做,这样在并发状况下能够保证只有一个线程会执行,在判断一次当前done
字段是否发生改变(这里确定有朋友会感到疑惑,为何这里还要在判断一次flag
?由于若是同时有两个goroutine调用这一行代码,一个goroutine成功CAS设置了标志的话,就会调用f,作资源初始化或者其它的一些事情,这个执行可能会耗费一段时间。同时另一个goroutine设置不成功,它想固然的认为另一个goroutine已经执行了f,可是实际上f可能尚未执行完,这就可能代码并发的问题。因此这里是为了保证当前代码块已经执行完),若是未发生改变,则开始执行代码块,代码块运行结束后会对done
字段作原子操做,标识该代码块已经被执行过了.若是让你本身写一个这样的库,你会考虑的这样全面吗?相信聪明的大家也必定会写出这样一段代码。若是要是我来写,上面的代码可能都同样,可是在if o.done == 0
这里我可能会采用CAS
原子操做来代替这个判断,以下:架构
type MyOnce struct { flag uint32 lock sync.Mutex } func (m *MyOnce)Do(f func()) { if atomic.LoadUint32(&m.flag) == 0{ m.lock.Lock() defer m.lock.Unlock() if atomic.CompareAndSwapUint32(&m.flag,0,1){ f() } } } func testDo() { mOnce := MyOnce{} for i := 0;i<10;i++{ go func() { mOnce.Do(func() { fmt.Println("test my once only run once") }) }() } } func main() { testDo() time.Sleep(10 * time.Second) } // 运行结果: test my once only run once
我就说原子操做是并发编程的基础吧,你看没有错吧~。并发
上面咱们也看了源码的实现,如今咱们来看三道题,你认为他们的答案是多少?
sync.Once()
方法中传入的函数发生了panic
,重复传入还会执行吗?
func panicDo() { once := &sync.Once{} defer func() { if err := recover();err != nil{ once.Do(func() { fmt.Println("run in recover") }) } }() once.Do(func() { panic("panic i=0") }) }
sync.Once()
方法传入的函数中再次调用sync.Once()
方法会有什么问题吗?
func nestedDo() { once := &sync.Once{} once.Do(func() { once.Do(func() { fmt.Println("test nestedDo") }) }) }
改为这样呢?
func nestedDo() { once1 := &sync.Once{} once2 := &sync.Once{} once1.Do(func() { once2.Do(func() { fmt.Println("test nestedDo") }) }) }
在本文的最把上面三道题的答案公布一下吧:
sync.Once.Do
方法中传入的函数只会被执行一次,哪怕函数中发生了 panic
;do
方法会一直等doshow()
中锁的释放致使发生了死锁;test nestedDo
,once1,once2是两个对象,互不影响。因此sync.Once
是使方法只执行一次对象的实现。大家都作对了吗?
代码已上传:https://github.com/asong2020/...
好啦,这篇文章就到这里啦,素质三连(分享、点赞、在看)都是笔者持续创做更多优质内容的动力!
建立了一个Golang学习交流群,欢迎各位大佬们踊跃入群,咱们一块儿学习交流。入群方式:加我vx拉你入群,或者公众号获取入群二维码
结尾给你们发一个小福利吧,最近我在看[微服务架构设计模式]这一本书,讲的很好,本身也收集了一本PDF,有须要的小伙能够到自行下载。获取方式:关注公众号:[Golang梦工厂],后台回复:[微服务],便可获取。
我翻译了一份GIN中文文档,会按期进行维护,有须要的小伙伴后台回复[gin]便可下载。
翻译了一份Machinery中文文档,会按期进行维护,有须要的小伙伴们后台回复[machinery]便可获取。
我是asong,一名普普统统的程序猿,让咱们一块儿慢慢变强吧。欢迎各位的关注,咱们下期见~~~
推荐往期文章: