你真的了解 sync.Once 吗

Posted 好文收藏

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了你真的了解 sync.Once 吗相关的知识,希望对你有一定的参考价值。

是什么

引用官方描述的一段话,Once is a object that will perform exactly one action,即它是一个对象,它提供了保证某个动作只被执行一次的功能。最典型的场景当然就是单例对象的初始化操作。

咋么做

Once 的代码很简洁,从头到尾加注释不超过 70 行代码。对外暴露了一个唯一接口 Do(f func()) ,使用起来也是非常简单。

package main

import (
  "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 main

import (
  "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 main

import (
  "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 了。

这个问题的原因在于真正使用资源的时候,资源初始化还没到位,真是尴尬

以上是关于你真的了解 sync.Once 吗的主要内容,如果未能解决你的问题,请参考以下文章

你真的了解方法吗?

Go 源码分析:sync.Once

go sync.once用法

go sync.once用法

你真的了解低代码平台吗?

Golang Once源码解析