转型做go大略一个多月了吧,工作中也是边写边学,最近也是在极客工夫学习一些go相干课程,现学现用,源码在我 github 上:https://github.com/wuqinqiang...
是什么
援用官网形容的一段话,Once is a object that will perform exactly one action
,即它是一个对象,它提供了保障某个动作只被执行一次的性能。最典型的场景当然就是单例对象的初始化操作。
咋么做
Once
的代码很简洁,从头到尾加正文不超过 70 行代码。对外裸露了一个惟一接口 Do(f func())
,应用起来也是非常简单。
package mainimport ( "fmt" "sync")func main() { var once sync.Once fun1 := func() { fmt.Println("第一次打印") } once.Do(fun1) fun2 := func() { fmt.Println("第二次打印") } once.Do(fun2)}
在运行下面这段代码之后,从后果中你会发现只运行了 fun1
。这样看如同没什么问题,然而这段代码并不是并发的调用 Do()
,那就略微调整一下代码:
package mainimport ( "fmt" "sync" "time")func main() { var once sync.Once for i := 0; i < 5; i++ { go func(i int) { fun1 := func() { fmt.Printf("i:=%d\n", i) } once.Do(fun1) }(i) } // 为了避免主goroutine间接运行完了,啥都看不到 time.Sleep(50 * time.Millisecond)}
咱们开启了5个并发的 goroutine
,不论你咋么运行,始终只打印一次,至于 i
是多少,就看先执行的是哪个 g
了。Once
保障只有第一次调用 Do()
办法时,传递的 f
(无参数无返回值的函数) 才会执行,并且之后不论调用的参数是否扭转了,也不再执行。
咋么实现
在看一个性能的同时,其实咱们自身也能够站在技术的角度上来思考,如果是你,你会咋么实现这个 Once
。我感觉这是件很有意思的事件。
第一工夫想到的就是 go
中开箱即用的 sync.Mutex 的 Lock()
办法的第一段:
// Lock locks m.// If the lock is already in use, the calling goroutine// blocks until the mutex is available.func (m *Mutex) Lock() { // Fast path: grab unlocked mutex. if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) { ...... return } ......}
利用 atomic
的原子操作来实现这个需要。这的确能够保障只执行一次。然而也存在一个微小的坑,咱们来验证下:
package mainimport ( "fmt" "net" "sync/atomic" "time")type OnceA struct { done uint32}func (o *OnceA) Do(f func()) { if !atomic.CompareAndSwapUint32(&o.done, 0, 1) { return } f()}func main() { var once OnceA var conn net.Conn go func() { fun1 := func() { time.Sleep(5 * time.Second) //模仿初始化的速度很慢 conn, _ = net.DialTimeout("tcp", "baidu.com:80", time.Second) } once.Do(fun1) }() time.Sleep(500 * time.Millisecond) fun2 := func() { fmt.Println("执行fun2") conn, _ = net.DialTimeout("tcp", "baidu.com:80", time.Second) } //再调用do曾经查看到done为1了 once.Do(fun2) _, err := conn.Write([]byte("\"GET / HTTP/1.1\\r\\nHost: baidu.com\\r\\n Accept: */*\\r\\n\\r\\n\"")) if err != nil { fmt.Println("err:", err) }}
conn
是一个 net.Conn
的接口类型变量,这里为了达到成果,通过 sleep
模仿了初始化资源的耗时 ,当 fun2()
想要进行初始化的时候,未然发现 done
的值是 1 了,然而 fun1
初始化速度很慢,导致接下来操作 conn.Write
的时候,因为此时 conn
还是一个空资源,最终运行时抛出空指针的 panic
了。
这个问题的起因在于真正应用资源的时候,资源初始化还没到位,真是难堪????。
那么 Go 是如何防止这种问题的呢?
// Copyright 2009 The Go Authors. All rights reserved.// Use of this source code is governed by a BSD-style// license that can be found in the LICENSE file.package syncimport ( "sync/atomic")// Once is an object that will perform exactly one action.type Once struct { done uint32 m Mutex}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() }}
你看大佬都间接正文贴心的通知你 if atomic.CompareAndSwapUint32(&o.done, 0, 1)
这个不是正确的实现。并发的状况下,胜者取得调用 f ,然而第二个会间接返回,没有期待第一个初始化完结。
所以 Once
实现应用了一个互斥锁,互斥锁保障了只有一个 g
初始化,同时采取的是双查看的机制,再次判断 Once.done
是否为 0,如果为 0,代表第一次初始化,等到初始化完结之后,再开释锁。并发状况下,其余的 g
就会被阻塞在 o.m.Lock()
。
如何避坑
说是避坑,然而绝大多数的坑都是因为程序员本身代码问题所导致的,尽管有点难堪,但的确如此。 Once
的“坑” 还算少的,不像 sync.Mutex
和 Channel
那样,略微姿态不留神点就 panic
了。这一块后续再写文章介绍下。除了下面须要留神的应用资源的时候资源还未初始化实现的问题,在 Once
中还须要防止的是死锁问题。
// 因为嵌套调用 Do 外面的 lock导致死锁func ErrOne() { var o sync.Once o.Do(func() { o.Do(func() { fmt.Println("初始化") }) })}
这里 Do
调用了 f
,f
外面又调用了 Do
,最终导致死锁。我把下面的代码简化成上面这样
package mainimport "sync"func main() { var mu sync.Mutex mu.Lock() mu.Lock()}
防止这种谬误也很简略,不要在 f
函数中再次调用以后的 Once
即可。
延长
下面有提到过,Once.Do
因为某些起因导致初始化失败,然而原生的问题在于,后续再也没有机会执行同一个 Once.Do
了,产生这样的状况,现实的解决是,只有真正初始化胜利,才设置 Done
的值,并且如果初始化失败,理当告诉到上游服务,这样上游服务能够做一些重试机制或者异样解决等操作。
package mainimport ( "fmt" "io" "net" "os" "sync" "sync/atomic" "time")type Once struct { done uint32 m sync.Mutex}// 传入的f 有返回值,如果初始化失败,返回对应error,// Do办法再把这个err返回给上游服务func (o *Once) Do(f func() error) error { if atomic.LoadUint32(&o.done) == 1 { //fast path return nil } return o.doSlow(f)}func (o *Once) doSlow(f func() error) error { o.m.Lock() defer o.m.Unlock() var err error if o.done == 0 { //双查看,还没有初始化 err = f() if err == nil { // 只有真正初始化胜利才把 done 的值改成1 atomic.StoreUint32(&o.done, 1) } } return err}
咱们扭转了 f
函数,减少了一个返回值,在初始化失败之后返回给 Do
函数,由 Do
函数再把谬误返回给上游的调用方,把控制权交还给调用方做失败的解决。另外改变的一点是,只有真正初始化胜利之后才把 Done
的值改成 1。那么咱们能够简略的把下面的业务代码革新一下:
package mainimport ( "fmt" "io" "net" "os" "sync" "sync/atomic" "time")type Once struct { done uint32 m sync.Mutex}// 传入的f 有返回值,如果初始化失败,返回对应error,// Do办法再把这个err返回给上游服务func (o *Once) Do(fn func() error) error { if atomic.LoadUint32(&o.done) == 1 { return nil } return o.doSlow(fn)}func (o *Once) doSlow(fn func() error) error { o.m.Lock() defer o.m.Unlock() var err error if o.done == 0 { /双查看,还没有初始化 err = fn() if err == nil { // 只有真正初始化胜利才把 done 的值改成1 atomic.StoreUint32(&o.done, 1) } } return err}func main() { urls := []string{ "127.0.0.1:3453", "127.0.0.1:9002", "127.0.0.1:9003", "baidu.com:80", } var conn net.Conn var o Once count := 0 var err error for _, url := range urls { err := o.Do(func() error { count++ fmt.Printf("初始化%d次\n", count) conn, err = net.DialTimeout("tcp", url, time.Second) fmt.Println(err) return err }) if err == nil { break } if count == 3 { fmt.Println("初始化失败,不再重试") break } } if conn != nil { _, _ = conn.Write([]byte("GET / HTTP/1.1\r\nHost: google.com\r\n Accept: */*\r\n\r\n")) _, _ = io.Copy(os.Stdout, conn) }}
当咱们在应用一些开源工具时,只有业务须要,你能够革新各种你想要的货色。有时候,阻塞住你的,往往就是一身空想罢了。共勉.