Do you really understand sync.Once Is that right

Time:2021-1-25

I’ve been doing go for more than a month. I’m also learning while writing in my work. Recently, I’ve learned some go related courses during geek time. Now I’m learning and using them. The source code is on my GitHub:github.com/wuqinqiang/Go_Concurren…

What is it?

To quote an official description,Once is a object that will perform exactly one actionThat is, it is an object, which provides the function of ensuring that an action is executed only once. The most typical scenario is, of course, the initialization of singleton objects.

How to do it

OnceThe code is very concise, with no more than 70 lines of code annotated from the beginning to the end. A unique interface is exposedDo(f func())It is also very simple to use.

package main

import (
  "fmt"
  "sync"
)

func main() {
  var once sync.Once
  fun1 := func() {
    fmt.Println (first print)
  }
  once.Do(fun1)

  fun2 := func() {
    fmt.Println (second printing)
  }

  once.Do(fun2)
}

After running the above code, you will find that only thefun1. This seems to be OK, but this code is not a concurrent callDo(), adjust the code slightly:

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)
  }
  //In order to prevent the main goroutine running directly, nothing can be seen
  time.Sleep(50 * time.Millisecond)
}

We have five concurrentgoroutineNo matter how you run it, you only print it onceiIt depends on which one is executed firstgIt’s too late.OnceGuarantee only the first callDo()Method, passedf(function with no parameter and no return value) will be executed, and will not be executed after that, regardless of whether the called parameter has changed.

How to realize it

While looking at a function, we can actually think from the perspective of technology. If it was you, how would you achieve thisOnce. I think it’s a very interesting thing.

The first thing I thought of wasgoOut of the box sync.Mutex OfLock()The first paragraph of the method:

// 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
  }
   ......
}

utilizeatomicTo achieve this requirement. This really guarantees that it will only be executed once. But there is also a huge pit. Let’s test it

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 )// simulation initialization is slow
      conn, _ = net.DialTimeout("tcp", "baidu.com:80", time.Second)
    }
    once.Do(fun1)
  }()
  time.Sleep(500 * time.Millisecond)
  fun2 := func() {
    fmt.Println (execution fun2)
    conn, _ = net.DialTimeout("tcp", "baidu.com:80", time.Second)
  }
  //Call do again to check that done is 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)
  }
}

connIt’s anet.ConnInterface type variable, here in order to achieve the effect, through thesleepThe time consumption of resource initialization is simulatedfun2()When you want to initialize, you have found thatdoneThe value of is 1, butfun1The initialization speed is very slow, leading to the next operationconn.WriteAt this time, because at this timeconnIt is also an empty resource, and eventually the runtime throws a null pointerpanicIt’s too late.

The reason for this problem is that the resource initialization is not in place when the resource is used. It’s really embarrassing.

So how does go avoid this problem?

// 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 sync

import (
  "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()
  }
}

You see, the big guys tell you directly and intimatelyif atomic.CompareAndSwapUint32(&o.done, 0, 1)This is not the right implementation. In the case of concurrency, the winner gets to call F, but the second one returns directly without waiting for the end of the first initialization.

thereforeOnceThe implementation uses a mutex, which ensures that there is only onegAt the same time, it adopts the double check mechanism to judge againOnce.doneWhether it is 0. If it is 0, it means the first initialization. After the initialization, the lock will be released. In the case of concurrency, othersgWill be blocked in theo.m.Lock()

How to avoid the pit

It is said to avoid the pit, but the vast majority of the pit is caused by the programmer’s own code problems, although it is a bit embarrassing, but it is true.OnceIt’s not like there’s a lot of “holes” in itsync.MutexandChannelThat way, if you don’t pay attention to your posture a little bit, it’s OKpanicIt’s too late. I’ll write an article about this later. In addition to the above problem that the resource has not been initialized when using the resource, you should pay attention toOnceWhat we need to avoid is deadlock.

//Deadlock caused by nested call to lock in do
func ErrOne() {
  var o sync.Once
  o.Do(func() {
    o.Do(func() {
      fmt.Println (initialization)
    })
  })
}

hereDoCalledffIt’s called againDo, leading to a deadlock. I simplified the above code to the following

package main

import "sync"

func main() {
  var mu sync.Mutex
  mu.Lock()
  mu.Lock()
}

It’s also very easy to avoid this kind of mistake. Don’t make mistakes in thefFunction to call the currentOnceThat’s it.

extend

As mentioned above,Once.DoInitialization failed for some reason, but the problem with the native is that there is no chance to execute the same program laterOnce.DoIf this happens, the ideal processing is to set it only after the initialization is successfulDoneIf the initialization fails, the upstream service should be notified, so that the upstream service can do some operations such as retrial mechanism or exception handling.

package main
​
import (
  "fmt"
  "io"
  "net"
  "os"
  "sync"
  "sync/atomic"
  "time"
)
​
type Once struct {
  done uint32
  m    sync.Mutex
}
//The f passed in has a return value. If initialization fails, the corresponding error is returned,
//The do method returns the err to the upstream service
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 {// double check, not initialized
    err = f()
    If err = = nil {// change the value of done to 1 only after the initialization is successful
      atomic.StoreUint32(&o.done, 1)
    }
  }
  return err
}

We have changedfFunction, adding a return value to theDoFunction, byDoThe function returns the error to the upstream caller and gives control back to the caller for failure handling. Another change is that only after the initialization is successful can theDoneChange the value of to 1. Then we can simply transform the above business code:

package main

import (
  "fmt"
  "io"
  "net"
  "os"
  "sync"
  "sync/atomic"
  "time"
)

type Once struct {
  done uint32
  m    sync.Mutex
}
//The f passed in has a return value. If initialization fails, the corresponding error is returned,
//The do method returns the err to the upstream service
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 {/ double check, not initialized
    err = fn()
    If err = = nil {// change the value of done to 1 only after the initialization is successful
      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 (initialize% d times, count)
      conn, err = net.DialTimeout("tcp", url, time.Second)
      fmt.Println(err)
      return err
    })
    if err == nil {
      break
    }
    if count == 3 {
      fmt.Println ("initialization failed, don't try again")
      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)
  }

}

When we use some open source tools, you can transform everything you want as long as the business needs. Sometimes, blocking you, is often a utopian just. Encourage each other

This work adoptsCC agreementReprint must indicate the author and the link of this article

Wu qinkuli