「Golang」sync.Once用法以及源码讲解

前言

在咱们开发过程当中常常会使用到单例模式这一经典的设计模式,单例模式能够帮助开发者针对某个(些)变量或者对象或者函数(方法)进行在程序运行期间只有一次的初始化或者函数调用操做,好比在开发项目中针对某一类链接池的初始化(如数据库链接池等)。针对这种状况,咱们就须要使用单例模式进行操做。

单例模式🌰

本身搞得单例模式

要实现一个单例模式,咱们会很快就想到了在一个结构体中放置一个flag字段用于标记当前的函数是否被执行过,举个🌰:数据库

`type SingletonPattern struct {`
 `done bool`
`}`
`func (receiver *SingletonPattern) Do(f func())  {`
 `if !receiver.done {`
 `f()`
 `receiver.done=true`
 `}`
`}`

看似很美好,可是此时,若是传入的须要调用的函数f()会执行很长时间,好比数据库查询或者作一些链接什么的,当别的goroutine运行到此处的时候因为尚未执行完f(),就会发现done标记仍然是false,那么仍然会调用一次f(),此时就违背了单例模式的初衷。设计模式

那么如何解决上面的并发的问题呢。此时就可使用go标准库中所提供的并发原语---sync.Once并发

标准库真香系列之sync.Once

话很少说先上sync.Once 结构体的源代码:tcp

`type Once struct {`
 `// 标记符号,用于标记是否执行过`
 `done uint32`
 `// 互斥锁,用于保护并发调用以及防止copy`
 `m    Mutex`
`}`

结构体就这么简单,字段done用于标记是否执行过函数,至于为何使用uint32类型,做者的理解是为了以后使用atomic操做作的妥协,m字段值用于保护并发状况下的情形,而且因为继承了Locker接口能够经过vet校验到其是否被复制函数

接下来看一下用于执行函数调用的Do()函数的实现:性能

`func (o *Once) Do(f func()) {`
 `// 原子获取当前 done 字段是否等于0`
 `// 若是当前字段等于1` 
 `// 则表明已经 执行过`
 `// 这是第一层校验`
 `if atomic.LoadUint32(&o.done) == 0 {`
 `// 若是为0则表明没被调用过则调用`
 `// 此处写成一个函数的缘由是为了`
 `// 进行函数内联提高性能`
 `o.doSlow(f)`
 `}`
`}`
`func (o *Once) doSlow(f func()) {`
 `// 此处加锁用于防止其余goroutine同时访问调用`
 `o.m.Lock()`
 `defer o.m.Unlock()`
 `// 二次校验`
 `// 为的是防止多个goroutine进入此函数的时候,可能发生的重复执行 f()`
 `if o.done == 0 {`
 `// 函数执行结束设置done 字段为 1表明已经执行完毕`
 `defer atomic.StoreUint32(&o.done, 1)`
 `// 执行`
 `f()`
 `}`
`}`

此时,sync.Once 的全部源代码已经解析完毕了(惊不惊喜,意不意外),其实sync.Once 的过程很简单,就是根据标记进行双重判断肯定函数是否执行过,没执行就执行,执行了就跳过。ui

sync.Once 的使用问题

哪来的deadlock?

sync.Once 的确很简单,使用也很简单,可是仍是会有使用上可能出现的一些问题好比下列代码:atom

`func main() {`
 `var once sync.Once`
 `once.Do(`
 `func() {`
 `fmt.Println("one once do")`
 `once.Do(`
 `func() {`
 `fmt.Println("second once do")`
 `})`
 `})`
`}`

该代码会出现什么问题?答案是:设计

fatal error: all goroutines are asleep - deadlock!

为何会这样?由于内层个Do是被外层的同一个once对象所调用,因为此时已经进入了第一个Do而且已经调用了函数,那么此时sync.Once 中的互斥锁字段,已经被加了锁,此时二次加锁就会产生死锁。所以使用sync.Once 最重要的一点就是:*指针

不要在执行函数中,嵌套当前的sync.Once 对象 不要在执行函数中,嵌套当前的sync.Once 对象 不要在执行函数中,嵌套当前的sync.Once 对象。(重要的话要说三遍)

哪来的invalid memory address or nil pointer dereference?

看一下下面的代码:

`func main() {`
 `var once sync.Once`
 `var conn net.Conn`
 `once.Do(`
 `func() {`
 `var err error`
 `conn, err = net.Dial("tcp", "")`
 `if err != nil {`
 `return`
 `}`
 `})`
 `conn.RemoteAddr()`
`}`

在运行时,会出现:

panic: runtime error: invalid memory address or nil pointer dereference

为何?由于sync.Once只保证执行一次,可是不保证执行是否出错,即我只管调用,出错了跟我无关,上述代码中

`conn, err = net.Dial("tcp", "")`

一定出现err!=nil的状况,此时若是不对conn变量进行判断为nil,就会出现空指针异常,那么,如何来保证他执行成功了呢,咱们须要对其进行改造

`type Once struct {`
 `once sync.Once`
`}`
`func (receiver *Once) OnceDo(f func() error) error {`
 `var err error`
 `receiver.once.Do(`
 `func() {`
 `err = f()`
 `})`
 `return err`
`}`
`func main() {`
 `var once Once`
 `var conn net.Conn`
 `err := once.OnceDo(`
 `func() error {`
 `var err error`
 `conn, err = net.Dial("tcp", "")`
 `if err != nil {`
 `return err`
 `}`
 `return nil`
 `})`
 `if err != nil {`
 `log.Fatal(err)`
 `}`
`}`

通过封装,咱们就能够获得sync.Once 执行时是否出错,以适配各类错误处理。

此封装可能会有更好的解决方案,上面的方案也仅仅是一个🌰罢了。

总结

至此sync.Once 的用法以及源码解析就完成了,可能有些地方有些理解上的错误,请各位谅解而且帮忙指出修改意见,若是这篇文章能帮到你,这是个人荣幸。

相关文章
相关标签/搜索