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.
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
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 is1
, it means that the function has been executed; if it is0
, it means that it has not been executed; -
m
is a mutex lock, which means thatsync.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 thedone
variable. Ifdone The value of
is 1, indicating that the operation has been executed. At this time, it returns directly and thedoSlow
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 ofdone
is still 0, it means that thef
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:
- Will there be any problem if the
sync.Once()
method is called again in the function passed in by thesync.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.
-
The function passed in the
sync.Once()
method haspanic
. 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
Iff
appearspanic
during execution, it will not be executed again; therefore, nothing will be printed. , the function passed in thesync.Once.Do
method will only be executed once, even if apanic
occurs in the function; -
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 printtest
.once1
,once2
are two objects and do not affect each other. Sosync.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