Introduction
In some scenarios, we need to initialize some resources, such as singleton objects, configurations, etc. There are many ways to initialize resources, such as definitionspackage
Level variables,init
Initialize the function, or inmain
Initializes the function. All three methods can ensure concurrency security and complete resource initialization at the start of the program.
However, sometimes we want to use delayed initialization to initialize when we really need resources, which requires concurrency security, in which case,Go
In languageProvide an elegant and concurrent and secure solution, which will be described in this article.
Basic concepts
What is
yes
Go
A synchronous primitive in the language that ensures that an operation or function is executed only once in a concurrent environment. It only has one export method, i.e.Do
, this method receives a function parameter. existDo
After the method is called, the function will be executed and will only be executed once, even if multiple coroutines are called simultaneously.
Application scenarios
Mainly used in the following scenarios:
- Singleton mode: Ensure that there is only one instance object globally and avoid repeated creation of resources.
- Delay initialization: When a resource is needed during the program operation,
Dynamically initialize the resource.
- Operations that only perform once: for example, only once configuration loading, data cleaning and other operations are required.
Application example
Singleton mode
In singleton mode, we need to make sure that a structure is initialized only once. useThis can be achieved easily.
package main import ( "fmt" "sync" ) type Singleton struct{} var ( instance *Singleton once ) func GetInstance() *Singleton { (func() { instance = &Singleton{} }) return instance } func main() { var wg for i := 0; i < 5; i++ { (1) go func() { defer () s := GetInstance() ("Singleton instance address: %p\n", s) }() } () }
In the above code,GetInstance
Function pass()
make sureinstance
Will be initialized only once. In a concurrent environment, multiple coroutines are called simultaneouslyGetInstance
When only one coroutine will executeinstance = &Singleton{}
, all coroutines get instancess
They are all the same.
Delay initialization
Sometimes it is hoped that certain resources will be initialized only when needed. useThis can be achieved.
package main import ( "fmt" "sync" ) type Config struct { config map[string]string } var ( config *Config once ) func GetConfig() *Config { (func() { ("init config...") config = &Config{ config: map[string]string{ "c1": "v1", "c2": "v2", }, } }) return config } func main() { // The configuration information is needed for the first time and initialize config cfg := GetConfig() ("c1: ", ["c1"]) // It is required for the second time. At this time, config has been initialized, and there is no need to initialize again. cfg2 := GetConfig() ("c2: ", ["c2"]) }
In this example, aConfig
Structure, which contains some setup information. useTo achieve
GetConfig
function, which is initialized on the first callConfig
. This way, we can initialize it when we really need itConfig
, thereby avoiding unnecessary overhead.
Implementation principle
type Once struct { // Indicates whether the operation has been performed done uint32 // Mutex lock ensures that only one coroutine can perform operations when multiple coroutines are accessed m Mutex } func (o *Once) Do(f func()) { //Judge the value of done. If it is 0, it means that f has not been executed yet if atomic.LoadUint32(&) == 0 { // Build slow paths (slow-path) to allow inline to fast paths (fast-path) of Do methods (f) } } func (o *Once) doSlow(f func()) { // Add lock () defer () // Double check to avoid f has been executed if == 0 { // Modify the value of done defer atomic.StoreUint32(&, 1) // Execute function f() } }
The structure contains two fields:
done
andmu
。done
It's oneuint32
A variable of type, used to indicate whether the operation has been executed;m
is a mutex that ensures that only one coroutine can perform operations when multiple coroutines are accessed.
The structure contains two methods:
Do
anddoSlow
。Do
The method is its core method, which receives a function parameterf
. First it will operate through atomicatomic.LoadUint32
(Ensure concurrency safety) Checkdone
If the value is 0, it meansf
The function has not been executed, and then executeddoSlow
method.
existdoSlow
In the method, first of all, the mutex lock ism
Locking to ensure that only one coroutine can execute when multiple coroutines are accessedf
function. Then check againdone
The value of the variable, ifdone
The value of still 0 is 0, indicatingf
The function has not been executed, and it is executed at this timef
Function, finally by atomic operationatomic.StoreUint32
Willdone
The value of the variable is set to 1.
Why encapsulate a doSlow method
doSlow
The existence of methods is mainly for performance optimization. Will slow path(slow-path
) code fromDo
Separate it from the method so thatDo
Fast path to the method (fast-path
) can be inlined (inlined
) , thereby improving performance.
Why is there a double check written?
From the source code, there are two pairsdone
judging the value of .
-
First check: Use atomic loading operation before acquiring the lock
atomic.LoadUint32
examinedone
The value of the variable, ifdone
The value of 1 means that the operation has been executed. It is returned directly at this time and will not be executed againdoSlow
method. This check can avoid unnecessary lock competition. -
Second inspection: After obtaining the lock, check again
done
The value of the variable is checked to ensure that other coroutines have not executed during the current coroutine acquisition lock.f
function. ifdone
The value off
The function has not been executed.
With double checking, lock competition can be avoided in most cases and improved performance.
Strengthened
Provided
Do
The method does not return a value, meaning if the function we pass in occurserror
Causes initialization failure, subsequent callsDo
The method will not be initialized again. To avoid this problem, we can implement a similarconcurrent primitives.
package main import ( "sync" "sync/atomic" ) type Once struct { done uint32 m } func (o *Once) Do(f func() error) error { if atomic.LoadUint32(&) == 0 { return (f) } return nil } func (o *Once) doSlow(f func() error) error { () defer () var err error if == 0 { err = f() // Only when there is no error, the value of done is modified if err == nil { atomic.StoreUint32(&, 1) } } return err }
The above code implements an enhancedOnce
Structure. With standardDifferently, this implementation allows
Do
The function parameter of the method returns aerror
. If the execution function does not returnerror
, modifydone
The value of , indicates that the function has been executed. In this way, in subsequent calls, only if nothing happenserror
Only when the function is executed, the function will be skipped to avoid initialization failure.
Things to note
Deadlock
Through analysisThe source code of
m
mutex field. When we areDo
Repeated calls within the methodDo
When the method is used, the same lock will be acquired multiple times. butmutex
Mutex does not support reentrability, so this will lead to deadlock.
func main() { once := {} (func() { (func() { ("init...") }) }) }
Initialization failed
The initialization failure here refers to the callDo
After the method, executef
Occurs during function processerror
, resulting in execution failure, existingWe cannot perceive the initialization failure in design. In order to solve this problem, we can implement a similar
Strengthening
once
, the previous content has provided specific implementation.
summary
This article introduces in detailGo
In language, including its basic definition, usage scenarios and application examples, as well as source code analysis, etc. In actual development,
Often used to implement singleton mode and delay initialization operations.
AlthoughSimple and efficient, but incorrect use may cause some unexpected situations and require extra caution.
Anyway,yes
Go
A very practical concurrency primitive in this article can help developers achieve secure operations in various concurrent scenarios. If you encounter a scenario that only needs to be initialized once,It's a very good choice.
The above is a detailed explanation of the usage of Golang concurrency tools. For more information about Golang, please pay attention to my other related articles!