博主介绍:

– 我是了 凡 微信公众号【了凡银河系】期待你的关注。未来大家一起加油啊~


前言


文章目录

  • 前言
  • Once是什么能做什么?
  • Once基础用法以及使用场景
    • 总结
  • Once如何实现
  • 使用Once的错误
    • 第一种:死锁
    • 第二种: 未初始化
  • 总结

Once是什么能做什么?

Once可以用来执行且仅仅执行一次动作,常常用于单例对象的初始化场景。

初始化单例资源有很多方法,比如定义package级别的变量,这样程序在启动的时候就可以初始化:

package abcimport timevar startTime = time.Now()

或者在init函数中进行初始化:

package abcvar startTime time.Timefunc init(){startTtime = time.Now()
}

又或者在main函数开始执行的时候,执行一个初始化的函数:

package abcvar startTime = time.Now()func initApp() {startTtime = time.Now()
}
func main() {initApp()
}

这三种方法都是线程安全的,并且后两种方法还可以根据传入的参数实现定制化的初始化操作。(可是这个和Once有什么关系呢,别着急,记住Once可以用来执行且仅仅执行一次动作

但是很多时候我们是要延迟初始化的,所以有时候单列资源的初始化,我们会使用下面的方法:

// 使用互斥锁保证线程(goroutine)安全
var connMu sync.Mutex
var conn net.Connfunc getConn() net.Conn {connMu.Lock()defer connMu.Unlock()// 返回已创建好的连接if conn != nil {return conn}// 创建连接conn, _ = net.DialTimeout("tcp","baidu.com:80", 10*time.Second)return conn
}func main() {conn := getConn()if conn == nil {panic("conn is nil")}
}

这种方式虽然实现起来简单,但是有性能问题。一旦连接创建好,每次请求的时候还是得竞争锁才能读取到这个连接,这是比较浪费资源的,因为连接如果创建好之后,其实不需要锁的保护了。所以Once并发原语就体现出它的价值了。


Once基础用法以及使用场景

sync.Once只暴露了一个方法Do,你可以多次调用Do方法,但是只有第一次调用Do方法时f参数才会执行,这里的f是一个无参数无返回值的函数。

func (0 *Once) Do(f func())

因为当且仅当第一次调用Do方法的时候参数f才会执行,即使第二次、第三次、第n次调用时f参数的值不一样,也不会被执行,比如下面的例子,虽然f1和f2是不同的函数,但是第二个函数f2就不会执行。

func main() {var once sync.Once// 第一个初始化函数f1 := func() {fmt.Println("in f1")}once.Do(f1) // 打印出 in f1// 第二个初始化函数f2 := func() {fmt.Println("in f2")}once.Do(f2) // 无输出
}

因为这里的f参数是一个无参数无返回的函数,所以你可能会通过闭包的方式引用的参数,比如:

var adr = "baidu.com"var conn net.Conn
var err erroronce.Do(func() {conn, err = net.Dial("tcp", addr)
})

而且在实际的使用中,绝大多数情况下,你会使用闭包的方式去初始化外部的一个资源。

Once的使用场景很明确,在标准库内部实现中也常常看到Once的身影

例如标准库内部 cache的实现上,就使用了Once初始化Cache资源,包括defaultDir值的获取:

func Default() *cache.Cache { // 获取默认的CachedefaultOnce.Do(initDefaultCache) // 初始化cachereturn defaultCache
}// 定义一个全局的cache变量,使用Once初始化,所以也定义了一个Once变量
var (defaultOnce  sync.OncedefaultCache *cache.Cache
)func initDefaultCache()  { // 初始化cache,也就是Once.Do使用的f函数.......defaultCache = c
}// 其他一些Once初始化变量,比如defaultDir
var (defaultDirOnce  sync.OncedefaultDir        stringdefaultDirErr   error
)

还有一些测试的时候初始化测试的资源(export_windows_test):

// 测试window系统调用时区相关函数
func ForceAusFromTZIForTesting() {ResetLocalOnceForTest()// 使用Once执行一次初始化localOnce.Do(func() { initLocalFromTZI(&aus) })
}

除此之外,还要保证只调用一次copyenv的envOnce,strings包下的Replacer,time包中的测试,Go拉取库时的proxy,net.pipe,crc64,Regexp,…,数不胜数。了解一个math/big/sqrt.go中实现的一个数据结构,它通过Once封装了一个只初始化一次的值:

// 值是3.0或者0.0的一个数据结构
var threeOnce struct {sync.Oncev *Float
}// 返回此数据结构的值,如果还没有初始化为3.0, 则初始化
func three() *Float {threeOnce.Do(func() { // 使用Once初始化threeOnce.v = NewFloat(3.0)}
}

将sync.Once和*Float封装成一个对象,提供了只初始化一次的值v。看它的three方法的实现,虽然每次都调用threeOnce.Do方法,但是参数只会调用一次。

总结

Once并发原语解决的问题和使用场景:Once常常用来初始化单例资源,或者并发访问只需初始化一次的共享资源,或者在测试的时候初始化一次测试资源。


Once如何实现

通过上述的问题你大概应该明白Once是什么以及怎么用了,但是实现的话,也许会像我们想的很简单,比如只需使用一个flag标记是否初始化过即可,最多是用atomic原子操作这个flag,比如下面的实现:

type Once struct {done uint32
}func (o *Once) Do(f func()) {if !atomic.CompareAndSwapUint32(&o.done, 0, 1) {return}f()
}

这样是一种实现方式,但是,这个实现有一个很大的问题,就是如果参数f执行很慢的话,后续调用Do方法的goroutine虽然看到done已经设置为执行过了,但是获取某些初始化资源的时候可能会得到空的资源,因为f还没有执行完。

所以,一个正确的Once实现要使用一个互斥锁,这样初始化的时候如果有并发的goroutine,就会进入doSlow方法。互斥锁的机制保证只有一个goroutine进行初始化,同时利用双检查的机制(double-checking),再次判断o.done是否为0,如果为0,则是第一次执行,执行完毕后,就将o.done设置为1,然后释放锁。

即使此时有多个goroutine同时进入了doSlow方法,因为双检查的机制,后续的goroutine会看到o.done的值为1,也不会再次执行f。

这样的话既保证了并发的goroutine会等待f完成,而且还不会多次执行f。

type Once struct {done uint32m  sync.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()}
}

这样呢,基本上我认为了解基本可以了,尽管有些地方还不是很懂,但是对于Once已经可以称为了解了。


使用Once的错误

第一种:死锁

Do方法会执行一次f,但是如果f中再次调用这个Once的Do方法的话,就会导致死锁的情况出现。这还不是无限递归的情况,而是Lock的递归调用导致的死锁。

func main() {var once sync.onceonce.Do(func() {once.Do(func() {fmt.Println("初始化")})})
}

想要避免这种情况的出现,就不要在f参数中调用当前的这个Once,不管是直接的还是间接的。

第二种: 未初始化

如果f方法执行的时候panic,或者f执行初始化资源的时候失败了,这个时候,Once还是会认为已经成功了,即使再次调用方法,也不会再次执行f。

比如:由于一些防火墙的原语,googleConn并没有被正确的初始化,后面如果想当然认为既然执行了Do方法googleConn就已经初始化的话,会抛出空指针的错误:

func main() {var once sync.Once var googleConn net.Conn // 到Google网站的一个连接once.Do(func() {// 建立到google.com的连接,有可能因为网络的原语,googleConn并没有建立成功,此时它的值为nilgoogleConn,_ = net.Dial("tcp","google.com:80")})// 发送http请求googleConn.Write([]byte("GET / HTTP/1.1\r\nHost: google.com/r/n Accept: */*\r\n\r\n"))io.Copy(os.Stdout.GoogleConn)
}

执行过Once.Do方法也可能因为函数执行失败的原因未初始化资源,并且以后也没有机会再次初始化资源,那么这种初始化未完成的问题该怎么解决呢?

解法:可以自己实现一个类似Once的并发原语,既可以返回当前调用Do方法是否正确完成,还可以在初始化失败后调用Do方法再次尝试初始化,直到初始化成功才不再初始化了。

// 一个功能更加强大的Once
type Once struct {m sync.Mutexdone uint32
}// 传入的函数f有返回值error,如果初始化失败,需要返回失败的error
// Do方法会把这个error返回给调用者
func (o *Once) Do(f fucn() error) error {if atomic.LoadUint32(&o.done) == 1 { // fast pathreturn nil}return o.slowDo(f)
}
// 如果还没有初始化
func (o *Once) slowDo(f func() error) error {o.m.Lock()defer o.m.Unlock()var err errorif o.done == 0 { // 双检查,还没有初始化err = f()if err == nil { // 初始化成功才将标记置为已初始化atomic.StoreUint32(&o.done, 1)}}return err
}

改变的地方就是Do方法和参数f函数都会返回error,如果f执行失败,会把这个错误信息返回。

对slowDo方法也做了调整,如果f调用失败,不会更改done字段的值,这样后续degoroutine还会继续调用f。如果f执行成功,才会修改done的值为1。

这个时候有了一个新的问题怎么查询是否初始化过呢?

type AnimalStore struct {once sync.Once; inited uint32
}
func (a *AnimalStore) Init() // 可以被并发调用a.once.Do(func() {longOperationSetupdbOpenFilesQueuesEtc()atomic.StoreUint32(&a.inited, 1)})
}
func (a *AnimalStore) CountOfCats() (int, error) { // 另外一个goroutineif atomic.LoadUint32(&a.inited) == 0 {return 0, NotYetInitedError}// Real operation
}

这样就解决掉了以上的问题

总结

Once常常用来实现单例模式

单例是23种设计模式之一,也是常常引起争议的设计模式之一,甚至有人把它归为反模式。为什么是反模式?(例如标准库钟的单例模式)

因为Go没有immutable类型,导致我们声明的全局变量都是可变的,别的地方或者第三方库可以随意更改这些变量。比如package io 中定义了几个全局变量,比如 io.EOF:

var EOF = errors.NEW("EOF")

因为它是一个package级别的变量,我们可以在程序中把它改了,这会导致一些依赖 io.EOF 这个变量做判断的代码出错。

io.EOF = errors.New("我们自己定义的EOF")
  • 一些单例(全局变量)的确很方便,比如Buffer池或者连接池,所以有时候我们也不要谈虎色变。虽然有人把单例模式称之为反模式,但毕竟只能代表一部分开发者的观点,否则越不会在23种设计模式中了。

  • 如果真的担心这个package级别的变量被人修改,可以不把它们暴露出来,而是提供一个只读的GetXXX的方法,这样别人就不会进行修改了。

  • 而且,Once不只应用于单例模式,一些变量在也需要在使用的时候做延迟初始化,所以也是可以使用Once处理这些场景的。

  • 所以,Once的应用场景很广泛。一旦遇到只需要初始化一次的场景,首先想到的就应该是Once并发原语


这次就先讲到这里,如果想要了解更多的golang语言内容一键三连后序每周持续更新!


【并发编程】Once 基本用法和如何实现以及常见错误相关推荐

  1. Day623.并发编程工具类库使用错误问题 -Java业务开发常见错误

    并发编程工具类库使用错误问题 多线程想必大家都知道,且JDK也为我们提供了很多并发编程的工具类库,接下来就是记录对应在业务开发中,可能会出现的并发编程工具类库使用错误的问题 一.线程复用导致信息错乱 ...

  2. 【并发编程】WaitGroup 基本用法和如何实现以及常见错误

    我是了 凡,微信公众号[了凡银河系]期待你的关注,内有资源相送.未来大家一起加油啊~ 前言 文章目录 前言 WaitGroup简单介绍 WaitGroup的基本用法 WaitGroup的实现 Add ...

  3. 【并发编程】Cond 基本用法和如何实现以及常见错误

    博主介绍: – 我是了 凡 微信公众号[了凡银河系]期待你的关注.未来大家一起加油啊~ 前言 一个常见的面试问题就是关于等待/通知(wait/notify)机制:例如请实现一个限定容量的队列(queu ...

  4. 它来了,阿里架构师的“Java多线程+并发编程”知识点详解手册,限时分享

    自学Java的时候,多线程和并发这一块可以说是最难掌握的部分了,很多小伙伴表示需要一些易于学习和上手的资料. 所以今天这份「Java并发学习手册」就是一份集中学习多线程和并发的手册,PDF版,由Red ...

  5. java多线程编程_阿里P8熬到秃头肝出来的:Java多线程+并发编程核心笔记

    自学Java的时候,多线程和并发这一块可以说是最难掌握的部分了,很多小伙伴表示需要一些易于学习和上手的资料. 所以今天这本「Java并发学习手册.pdf」就是一份集中学习多线程和并发的手册,PDF版, ...

  6. Java并发编程-Java内存模型(JMM)

    前言 在上一章 Java并发编程-Android的UI框架为什么是单线程的? 中笔者介绍了并发编程线程安全「三大恶」:「可见性」.「原子性」以及「有序性」 广义上来说,并发编程问题笔者归纳为:是由于后 ...

  7. 【并发编程】map 基本用法和常见错误以及如何实现线程安全的map类型

    博主介绍: – 我是了 凡 微信公众号[了凡银河系]期待你的关注.未来大家一起加油啊~ 前言 哈希表介绍 哈希表(Hash Table)这个数据结构,在Go语言基础的时候就已经涉及过了.实现的就是ke ...

  8. Python3 与 C# 并发编程之~ Net篇

    NetCore并发编程 示例代码:https://github.com/lotapp/BaseCode/tree/master/netcore/4_Concurrency 先简单说下概念(其实之前也有 ...

  9. 2w字 + 40张图带你参透并发编程!

    1  并发历史  在计算机最早期的时候,没有操作系统,执行程序只需要一种方式,那就是从头到尾依次执行.任何资源都会为这个程序服务,在计算机使用某些资源时,其他资源就会空闲,就会存在 浪费资源 的情况. ...

  10. Java并发编程:Thread类的使用

    为什么80%的码农都做不了架构师?>>>    Java并发编程:Thread类的使用 在前面2篇文章分别讲到了线程和进程的由来.以及如何在Java中怎么创建线程和进程.今天我们来学 ...

最新文章

  1. Hibernate Synchronizer3——一个和hibernate Tool类似的小插件之使用方法
  2. python 关于excelcsv与cookie的部分笔记
  3. csr8670 修改key_CSR8670 DFU user guide
  4. php面向对象之单表操作类
  5. docker 在window 10 专业版的安装 .net core 在docker的部署
  6. ggplot2 | 坐标标度函数、坐标系统函数
  7. Linux系统调用getuid的简单分析
  8. vue中watch监听路由传来的参数变化
  9. collections模块 :namedtuple、deque、defaultdict、OrderedDict、ChainMap、Counter
  10. 软件基本功:开发测试中的穷举归纳法
  11. 打狗棒法之:Cknife(C刀)自定义模式秒过安全狗(二)
  12. 企业如何从0到1搭建BI系统
  13. m6000查看端口状态_Linux查看端口使用状态、关闭端口方法
  14. OpenCV调用工业相机
  15. 上海立信会计师事务所专场 — 纯前端表格技术应用研讨会
  16. 杂评 360和腾讯之争
  17. GCS_SERVER_PROCESSES
  18. 使用Dev C++运行c语言代码时碰到Failed to executeC:\c++.cpp: Error 0 :操作成功完成
  19. vue修改预设preset
  20. poi excel 导出设置边框,自定义背景色,自定义字体

热门文章

  1. LED恒流驱动IC汇总
  2. php 获取搜索引擎,PHP获取搜索引擎关键词
  3. 使用pm2管理项目(指令)
  4. 参考文献起止页码怎么写_参考文献规范写法
  5. 英文连写字体怎么练_漂亮的英语字体是这样练成的!!
  6. python3打包exe失败_python3.7打包成exe就三步
  7. 生活中的算法的实际举例_生活中的算法
  8. 智慧工地帮助建筑企业高效实现工人实名制管理
  9. 数据结构:八大数据结构分类
  10. 概率的意义:随机世界与大数法则