Go synchronization primitive sync/Once

Basic usage

sync.Once Official descriptionOnce is an object that will perform exactly one action, that is, sync.Once is an object that provides a guarantee Each action is executed only once. Where do we need to ensure that an action is only executed once? This reminds us of the initialization of resources, which often uses the singleton pattern.

image-20230904111249267

Singleton Pattern is a design pattern that ensures that a class has only one instance and provides a global access point to obtain that instance. This ensures that there is only one shared instance throughout the entire program, avoiding unnecessary object creation and saving resources.

Singleton mode is usually used to manage global state, database connection pool, thread pool and other scenarios that require global uniqueness.

In the Go language, global variables are automatically initialized when the program starts. Therefore, if you assign a value to a global variable when it is defined, the creation of the object will also be completed when the program starts. This can be used to implement the singleton mode. The following is a sample code:

type MySingleton struct {<!-- -->
    //Field definition
}

var mySingletonInstance = & amp;MySingleton{<!-- -->
    //Initialize fields
}

func GetMySingletonInstance() *MySingleton {<!-- -->
    return mySingletonInstance
}

In the above code, we define a global variable mySingletonInstance and assign a value when defining it, thus completing the creation and initialization of the object when the program starts. In the GetMySingletonInstance function, we can directly return the global variable mySingletonInstance to implement the singleton mode.

We can use the init function to implement the singleton pattern. The init function is a function that is automatically executed when the package is loaded, so we can create and initialize the singleton object in it to ensure that the object is created when the program starts. Here is a sample code:

package main

type MySingleton struct {<!-- -->
    //Field definition
}

var mySingletonInstance *MySingleton

func init() {<!-- -->
    mySingletonInstance = & amp;MySingleton{<!-- -->
        //Initialize fields
    }
}

func GetMySingletonInstance() *MySingleton {<!-- -->
    return mySingletonInstance
}

In the above code, we define a package-level global variable mySingletonInstance, and create and initialize the object in the init function. In the GetMySingletonInstance function, we directly return the global variable to implement the singleton mode.

Of course, we can also use sync.Mutex using the Go language to implement the singleton mode. The following is a sample code:

package main

import (
"fmt"
"sync"
)

type Singleton struct {<!-- -->
data int
}

var (
once sync.Mutex
instance *Singleton
)

func GetInstance() *Singleton {<!-- -->
if instance == nil {<!-- -->
once.Lock()
defer once.Unlock()
if instance == nil {<!-- -->
instance = & amp;Singleton{<!-- -->data: 42}
}
}
return instance
}

func main() {<!-- -->
// Get singleton instance
singleton1 := GetInstance()
fmt.Println("Singleton 1 data:", singleton1.data)

// Get the singleton instance again, it should be the same instance
singleton2 := GetInstance()
fmt.Println("Singleton 2 data:", singleton2.data)

// Verify whether two instances are the same
if singleton1 == singleton2 {<!-- -->
fmt.Println("Singleton instances are the same.")
} else {<!-- -->
fmt.Println("Singleton instances are different.")
}
}

In the above example, we still use the package-level variable instance to save the singleton instance, and use sync.Mutex to ensure that there is only one in concurrent situations >goroutine can create instances. Although this approach works, it introduces an additional mutex that may have some impact on performance.

In practical applications, using sync.Once is still the more common and elegant choice.

sync.Once only provides one function to the outside world:

func (o *Once) Do(f func()) {<!-- -->...}

We only need to call it to use it. Let’s first look at an example to illustrate the characteristics of sync.Once, which ensures that an action is only executed once:

import (
"fmt"
"sync"
)

func f1() {<!-- -->
fmt.Println("This is f1 fun\r\\
")
}

func f2() {<!-- -->
fmt.Println("This is f2 fun\r\\
")
}

func f3() {<!-- -->
fmt.Println("This is f3 fun\r\\
")
}

func main() {<!-- -->
var once sync.Once
once.Do(f1)
once.Do(f2)
once.Do(f3)
}

#Results of the
This is f1 fun

The above code uses once.Do to call the functions f1, f2, and f3, but the result is only f1 function is executed, and the f2 and f3 functions are ignored.

You see, the usage scenario of sync.Once is very clear. It is often used to initialize single instance resources, or to concurrently access shared resources that only need to be initialized once, or to initialize test resources once during testing.

The following code is a singleton pattern rewritten by sync.Once:

package main

import (
"fmt"
"sync"
)

// Singleton is a singleton structure
type Singleton struct {<!-- -->
data int
}

var instance *Singleton
var once sync.Once

// GetInstance is used to obtain a singleton instance
func GetInstance() *Singleton {<!-- -->
once.Do(func() {<!-- -->
instance = & amp;Singleton{<!-- -->data: 42}
})
return instance
}

func main() {<!-- -->
// Get singleton instance
singleton1 := GetInstance()
fmt.Println("Singleton 1 data:", singleton1.data)

// Get the singleton instance again, it should be the same instance
singleton2 := GetInstance()
fmt.Println("Singleton 2 data:", singleton2.data)

// Verify whether two instances are the same
if singleton1 == singleton2 {<!-- -->
fmt.Println("Singleton instances are the same.")
} else {<!-- -->
fmt.Println("Singleton instances are different.")
}
}

In the above example, we defined a structure named Singleton to represent a singleton object. Get the singleton instance through the GetInstance function. In the GetInstance function, we use sync.Once to ensure that the instance will only be created once. This way, multiple calls to GetInstance will return the same instance.

Structure

The sync.Once structure is very simple, as follows:

type Once struct {<!-- -->
done uint32
m Mutex
}

The main fields are explained below:

  • done is used to determine whether the function is executed. If the value is 1, it means that the function has been executed; if it is 0, it means that it has not been executed;

  • m is a mutex lock, which means that sync.Once uses a lock to ensure synchronization.

Source code analysis

sync.Once is a method provided to the outside world:

func (o *Once) Do(f func()) {<!-- -->
    //Load the done value. If the value is 1, it will end directly. If the value is 0, it will enter the doSlow function.
if atomic.LoadUint32( & amp;o.done) == 0 {<!-- -->
o.doSlow(f)
}
}

In fact, in the Go source code comments, an incorrect method of using unused locks is also provided:

if atomic.CompareAndSwapUint32( & amp;o.done, 0, 1) {<!-- -->
    f()
}

Directly change the done value through the CompareAndSwapUint32 method of the atomic package. Although this can ensure that the operation is an atomic operation, the biggest problem is that if it is called concurrently, When one goroutine is executed, the other one will not wait for the success of the execution, but will return directly. This does not guarantee that the incoming method will be executed first.

In order to optimize performance, the atomic.LoadUint32 + doSlow method was introduced to change the slow path (slow-path) code from Do method, so that the fast path (fast-path) of the Do method can be inlined (inlined), thus improving performance.

Let’s look at the doSlow function:

func (o *Once) doSlow(f func()) {<!-- -->
o.m.Lock() //Lock
defer o.m.Unlock() //Unlock after function execution
    //If the o.done value is 0, call and execute the passed f function, and then change o.done to 1
if o.done == 0 {<!-- -->
defer atomic.StoreUint32( & amp;o.done, 1)
f()
}
}

A correct sync.Once implementation should use a mutex lock, so that if there is a concurrent goroutine during initialization, it will enter the doSlow method . The mutex lock mechanism ensures that only one goroutine is initialized, and at the same time, the double-checking mechanism (double-checking) is used to determine o.done again Is it 0? If it is 0, it is the first execution. After the execution is completed, o.done is set to 1 and then release the lock.

Why are there two judgments on the value of done?

  • First check: Before acquiring the lock, use the atomic load operation atomic.LoadUint32 to check the value of the done variable. If done The value of is 1, indicating that the operation has been executed. At this time, it returns directly and the doSlow method is no longer executed. This check avoids unnecessary lock contention.
  • Second check: After acquiring the lock, check the value of the done variable again. This check is to ensure that other coroutines have not been executed while the current coroutine acquires the lock. f function. If the value of done is still 0, it means that the f function has not been executed.

Through double checking, lock contention can be avoided in most cases and performance can be improved.

Summary

Let’s introduce some summary points through a few questions:

  1. Will there be any problem if the sync.Once() method is called again in the function passed in by the sync.Once() method?

The following code:

func main() {<!-- -->
   once := sync.Once{<!-- -->}
   once.Do(func() {<!-- -->
      once.Do(func() {<!-- -->
         fmt.Println("init...")
      })
   })
}

By analyzing the source code of sync.Once, you can see that it contains a mutex field named m. When we call the Do method repeatedly inside the Do method, we will try to acquire the same lock multiple times. However, the mutex mutex does not support reentrant operations, so this will lead to deadlock.

  1. The function passed in the sync.Once() method has panic. Will it still be executed if it is passed in repeatedly?

    The following code:

    func panicDo() {<!-- -->
     once := & amp;sync.Once{<!-- -->}
     defer func() {<!-- -->
      if err := recover();err != nil{<!-- -->
       once.Do(func() {<!-- -->
        fmt.Println("run in recover")
       })
      }
     }()
     once.Do(func() {<!-- -->
      panic("panic i=0")
     })
    }
    

    sync.Once.Do If f appears panic during execution, it will not be executed again; therefore, nothing will be printed. , the function passed in the sync.Once.Do method will only be executed once, even if a panic occurs in the function;

  2. What will the following function print?

    The following code:

    func nestedDo() {<!-- -->
     once1 := & amp;sync.Once{<!-- -->}
     once2 := & amp;sync.Once{<!-- -->}
     once1.Do(func() {<!-- -->
      once2.Do(func() {<!-- -->
       fmt.Println("test")
      })
     })
    }
    

    sync.Once ensures that the passed function will only be executed once, so print test.

    once1, once2 are two objects and do not affect each other. So sync.Once is an implementation of the object that makes the method execute only once.

Reference resources:

Chao Yuepan (Bird's Nest) https://time.geekbang.org/column/intro/100061801

mohuishou https://lailin.xyz/post/go-training-week3-once.html

syntaxbug.com © 2021 All Rights Reserved.