SoFunction
Updated on 2025-03-05

Go sync Once implementation principle example analysis

text

In many cases, we may need to control a piece of code to be executed only once, such as doing some initialization operations, such as initializing database connections, etc. For this scenario, go provides us withObject, it ensures that an action is executed only once. Of course we can pass it ourselves.MutexaccomplishThe function of , but it is a bit cumbersome, because we not only need to control the lock ourselves, but also use a logo to mark whether it has been executed.

Implementation of Once

OnceThe implementation is very simple. As follows, there are only 20 lines of code, but it contains some common processing methods for go concurrency and synchronization.

package sync
import (
   "sync/atomic"
)
type Once struct {
   done uint32
   m    Mutex
}
func (o *Once) Do(f func()) {
   if atomic.LoadUint32(&) == 0 {
      (f)
   }
}
func (o *Once) doSlow(f func()) {
   ()
   defer ()
   if  == 0 {
      defer atomic.StoreUint32(&, 1)
      f()
   }
}

Brief description:

  • doneThe field indicates whether the operation has been executed, that is, we pass it toDowhether the function has been executed.
  • DoThe method receives a function parameter, which will be executed only once.
  • OnceInternal is throughMutexto achieve synchronization between different coroutines.

Example of usage

In the following example,(test)Is executed 3 times, but in the endtestOnly executed once.

package sync
import (
   "fmt"
   "sync"
   "testing"
)
var once 
var a = 0
func test() {
   a++
}
func TestOnce(t *) {
   var wg 
   (3)
   for i := 0; i < 3; i++ {
      go func() {
         // Will call 3 times, but will only execute once in the end         (test)
         ()
      }()
   }
   ()
   (a) // 1
}

Some working mechanisms of Once

  • OnceofDoMethods can ensure that multiple goroutines are executed simultaneouslyDoWhen the method is taken, the firstDoBefore the execution of the goroutine with the execution rights returns, other goroutines will be blocked.There is only the first one on the callDoWhen the call returns, other goroutines can continue to be executed, and all other goroutines will no longer be executed and passed toDofunction. (If it is an initialization scenario, this can avoid performing other operations before the initialization is completed)
  • ifoccurpanicWhen it is passed toDoThe function is still marked as completed. Follow-upDoThe call will not be executed toDofunction parameters.
  • We can't simply passatomic.CompareAndSwapUint32To decide whether to executef(), because it cannot be guaranteed when multiple goroutines are executed simultaneouslyf()Only executed once. soOnceUsed insideMutex, so as to effectively protect the critical area.
// Error implementation, this cannot guarantee that f is executed only onceif atomic.CompareAndSwapUint32(&, 0, 1) {
    f()
}
  • The function parameters have no parameters. If we need to pass some parameters, we can use them againfMake a layer of parcel.
(func() { (filename) })

Once detailed explanation

hotpath

What is said herehotpathRefers toOnceThe first field indone

type Once struct {
   // hotpath
   done uint32
   m    Mutex
}

OnceThe first field of the structure isdone, this is becausedoneThe visit is much greater thanOnceAnother field inm, put in the first field, the compiler can do some optimizations, because the address of the structure is actually the address of the first field of the structure, so that it can be accesseddoneWhen using fields, you do not need to access them through the structure address + offset, which improves performance to a certain extent.

Structural address calculation example:

type person struct {
   name string
   age  int
}
func TestStruct(t *) {
   var p = person{
      name: "foo",
      age:  10,
   }
   // The address of p and   // 0xc0000100a8, 0xc0000100a8
   ("%p, %p\n", &p, &)
   // Address   // 0xc0000100b8
   ("%p\n", &)
   The address of // can also be calculated by: structure address + age field offset.   // 0xc0000100b8
   (((&p), ()))
}

atomic.LoadUint32

func (o *Once) Do(f func()) {
   if atomic.LoadUint32(&) == 0 {
      (f)
   }
}

existDoIn the method, it is throughatomic.LoadUint32To judgedoneWhether it is equal to 0, this is because if used directlydone == 0In the way, it may lead todoSlowInsidedoneAfter setting to 1,DoIt cannot be observed normally in the method. So useatomic.LoadUint32

And indoSlowIt can be passed insidedone == 0To judge, this is becausedoSlowIt has been passed insideMutexProtected. Unique settingsdone = 1The place is in the critical area, sodoSlowPass insidedone == 0It's completely no problem to judge.

atomic.StoreUint32

func (o *Once) doSlow(f func()) {
   ()
   defer ()
   if  == 0 {
      defer atomic.StoreUint32(&, 1)
      f()
   }
}

existdoSlowIn the method, setdoneFor 1, also byatomic.StoreUint32Set it up. This ensures that it is set updoneAfter 1, it can be seen by other goroutines in time.

Mutex

doSlowIn the realization, it will eventually be passedMutexto protect critical areas, throughMutexCan be achievedfOnly executed once, and other goroutines can be used this timefexecution results. Because other goroutines are on the first timefBefore the call returns, it is blocked and retrieved.Mutexlocked places when they getMutexWhen locking, you can continue to execute, but at this timefExecution has been completed, so when they are retrievedMutexAfter locking, nothing was done.

However, their blocking state is released and can continue to be executed.

Summarize

  • OnceIt ensures that the passed function will only be executed once, which is often used in some initialized scenarios or singleton modes.
  • OnceAll rights reservedDoThe concurrent calls are safe, allThe operation after the call will definitely be corrected on the first timefExecute after the call. (Not obtainedfThe goroutine of execution rights will block)
  • even thoughThe insidefAppearedpanic, will not be called again in the futuref

The above is the detailed content of the example analysis of the implementation principle of go sync Once. For more information on the implementation principle of go sync Once, please pay attention to my other related articles!