background
In some scenarios, we hope that a certain operation or function will be executed only once, such as the initialization of singleton mode, the loading of some resource configurations, etc.
This function is implemented in golang. Once only provides one Do method to the outside world. The Do method only receives one function parameter. It can ensure that in concurrent scenarios, when the Do method is called multiple times, the function corresponding to the parameter is executed only once.
Quick Start
Define an f1 function, enable 10 concurrency at the same time, and execute f1 function through the Do method provided by Once. Once can ensure that the f1 function is executed only once.
func TestOnce(t *) { once := {} f1 := func() { ("f1 func") } wg := {} for i := 0; i < 10; i++ { (1) go func() { defer () (f1) }() } () }
Source code analysis
golang version: 1.18.2
Source code path: src/sync/
// Once is an object that will perform exactly one action. // // A Once must not be copied after first use. type Once struct { // done indicates whether the action has been performed. // It is first in the struct because it is used in the hot path. // The hot path is inlined at every call site. // Placing done first allows more compact instructions on some architectures (amd64/386), // and fewer instructions (to calculate offset) on other architectures. done uint32 m Mutex } // Once only provides one Do method to the outside worldfunc (o *Once) Do(f func()) {}
- There are two fields inside Once: done and m
- done is used to indicate whether the passed function has been executed. When it is not executed and is being executed, done=0, and when the execution is completed, done=1
- m mutex lock is used to ensure that the passed function is executed only once when concurrent calls are called.
Do()
// Do calls the function f if and only if Do is being called for the // first time for this instance of Once. In other words, given // var once Once // if (f) is called multiple times, only the first call will invoke f, // even if f has a different value in each invocation. A new instance of // Once is required for each function to execute. // // Do is intended for initialization that must be run exactly once. Since f // is niladic, it may be necessary to use a function literal to capture the // arguments to a function to be invoked by Do: // (func() { (filename) }) // // Because no call to Do returns until the one call to f returns, if f causes // Do to be called, it will deadlock. // // If f panics, Do considers it to have returned; future calls of Do return // without calling f. // func (o *Once) Do(f func()) { // Note: Here is an incorrect implementation of Do: // // if atomic.CompareAndSwapUint32(&, 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(&) == 0 { // Outlined slow-path to allow inlining of the fast-path. (f) } } func (o *Once) doSlow(f func()) { () defer () if == 0 { defer atomic.StoreUint32(&, 1) f() } }
- First, quickly determine whether the passed function parameters have been executed by atomic.LoadUint32(&) == 0. If done=0, it means that the function is not executed or is being executed; if done=1, it means that the function has been executed, it will be quickly returned.
- Locking is done through m mutex to ensure concurrency security
- By == 0, confirm whether the passed function parameters have been executed. If done=0 at this time, because the previous step has been locked through m, it can be guaranteed that the passed function has not been executed yet. After executing the function, change done to 1; if done!=0 at this time, it means that other goroutines have successfully executed the function during the waiting period of lock, and you can return directly at this time.
Note: The same Once cannot be reused
func TestOnce(t *) { once := {} f1 := func() { ("f1 func") } f2 := func() { ("f2 func") } // F1 execution is successful (f1) // f2 will not be executed (f2) }
When defining two functions f1 and f2, the same Once is executed, the f1 function can only be guaranteed to be executed once.
It is guaranteed that the first incoming function parameter is executed only once, not that each incoming function parameter is executed only once, and the same Once cannot be reused. If you want both f1 and f2 to be executed only once, you can initialize two Once.
Note 2: Error implementation
if atomic.CompareAndSwapUint32(&, 0, 1) { f() }
Why is it wrong to implement it through CAS?
Because CAS can only guarantee that the function is executed once, but it cannot guarantee that when f() is still executed, other goroutines will wait for their execution to complete before returning. This is very important. When the function we pass in is a time-consuming operation, such as establishing a connection with db, we must wait for the function to be executed before returning, otherwise some unknown operations will occur
Note three: atomic.LoadUint32(&) == 0 and atomic.StoreUint32(&, 1)
Why use atomic.LoadUint32(&) == 0 to judge, instead of == 0 to judge
To prevent data competition, use == 0 to judge that data competition will occur (Data Race)
The data competition problem refers to at least two threads/coroutines that read and write a shared memory, and at least one thread/coroutines writes its shared memory.
When multiple threads/coroutines write to shared memory at the same time, during the writing process, other threads/coroutines read the data that is not expected in the memory data.
Verify data competition issues:
package main import ( "fmt" "sync" ) func main() { once := Once{} var wg (2) go func() { (print) () }() go func() { (print) () }() () ("end") } func print() { ("qqq") } type Once struct { done uint32 m } func (o *Once) Do(f func()) { // It turns out: atomic.LoadUint32(&) == 0 if == 0 { (f) } } func (o *Once) doSlow(f func()) { () defer () if == 0 { // It turns out: atomic.StoreUint32(&, 1) defer func() { = 1 }() f() } }
Execute the command:
go run -race
Execution results:
qqq ================== WARNING: DATA RACE Write at 0x00c0000bc014 by goroutine 7: main.(*Once).doSlow.func1() /Users/cr/Documents/golang/src//go/demo/:44 +0x32 () /usr/local/go/src/runtime/:436 +0x32 main.(*Once).Do() /Users/cr/Documents/golang/src//go/demo/:35 +0x52 .func1() /Users/cr/Documents/golang/src//go/demo/:13 +0x37 Previous read at 0x00c0000bc014 by goroutine 8: main.(*Once).Do() /Users/cr/Documents/golang/src//go/demo/:34 +0x3c .func2() /Users/cr/Documents/golang/src//go/demo/:17 +0x37 Goroutine 7 (running) created at: () /Users/cr/Documents/golang/src//go/demo/:12 +0x136 Goroutine 8 (running) created at: () /Users/cr/Documents/golang/src//go/demo/:16 +0x1da ================== end Found 1 data race(s) exit status 66
The above is the detailed content of the principle analysis of golang that is only executed once. For more information about golang execution, please pay attention to my other related articles!