SoFunction
Updated on 2025-03-05

Detailed explanation of the usage of Golang concurrency tool

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 definitionspackageLevel variables,initInitialize the function, or inmainInitializes 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,GoIn languageProvide an elegant and concurrent and secure solution, which will be described in this article.

Basic concepts

What is

yesGoA 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. existDoAfter 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,GetInstanceFunction pass()make sureinstanceWill be initialized only once. In a concurrent environment, multiple coroutines are called simultaneouslyGetInstanceWhen only one coroutine will executeinstance = &Singleton{}, all coroutines get instancessThey 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 = &amp;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, aConfigStructure, which contains some setup information. useTo achieveGetConfigfunction, 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(&amp;) == 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(&amp;, 1)
      // Execute function      f()
   }
}

The structure contains two fields:doneandmudoneIt's oneuint32A variable of type, used to indicate whether the operation has been executed;mis a mutex that ensures that only one coroutine can perform operations when multiple coroutines are accessed.

The structure contains two methods:DoanddoSlowDoThe method is its core method, which receives a function parameterf. First it will operate through atomicatomic.LoadUint32(Ensure concurrency safety) CheckdoneIf the value is 0, it meansfThe function has not been executed, and then executeddoSlowmethod.

existdoSlowIn the method, first of all, the mutex lock ismLocking to ensure that only one coroutine can execute when multiple coroutines are accessedffunction. Then check againdoneThe value of the variable, ifdoneThe value of still 0 is 0, indicatingfThe function has not been executed, and it is executed at this timefFunction, finally by atomic operationatomic.StoreUint32WilldoneThe value of the variable is set to 1.

Why encapsulate a doSlow method

doSlowThe existence of methods is mainly for performance optimization. Will slow path(slow-path) code fromDoSeparate it from the method so thatDoFast 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 pairsdonejudging the value of .

  • First check: Use atomic loading operation before acquiring the lockatomic.LoadUint32examinedoneThe value of the variable, ifdoneThe value of 1 means that the operation has been executed. It is returned directly at this time and will not be executed againdoSlowmethod. This check can avoid unnecessary lock competition.
  • Second inspection: After obtaining the lock, check againdoneThe value of the variable is checked to ensure that other coroutines have not executed during the current coroutine acquisition lock.ffunction. ifdoneThe value offThe function has not been executed.

With double checking, lock competition can be avoided in most cases and improved performance.

Strengthened

ProvidedDoThe method does not return a value, meaning if the function we pass in occurserrorCauses initialization failure, subsequent callsDoThe 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(&amp;) == 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(&amp;, 1)
      }
   }
   return err
}

The above code implements an enhancedOnceStructure. With standardDifferently, this implementation allowsDoThe function parameter of the method returns aerror. If the execution function does not returnerror, modifydoneThe value of , indicates that the function has been executed. In this way, in subsequent calls, only if nothing happenserrorOnly when the function is executed, the function will be skipped to avoid initialization failure.

Things to note

Deadlock

Through analysisThe source code ofmmutex field. When we areDoRepeated calls within the methodDoWhen the method is used, the same lock will be acquired multiple times. butmutexMutex 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 callDoAfter the method, executefOccurs 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 similarStrengtheningonce, the previous content has provided specific implementation.

summary

This article introduces in detailGoIn 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,yesGoA 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!