SoFunction
Updated on 2025-03-05

Detailed explanation of Goroutine, an important component that cannot be avoided by Golang,

Concurrency refers to the fact that a program is composed of several independently run codes, mainly relying on the parallel computing and scheduling capabilities of multi-core CPUs.

Golang has outstanding capabilities in concurrency and implements the concept of typical coroutines through Goroutinue. Golang's concurrency philosophy is to share memory through communication, not communicate through shared memory.

The difference between goroutinue and traditional threads

It is mainly reflected in four aspects:

1. Different memory occupancy: Goroutinue creates 2kb of memory and will automatically expand capacity when the stack space is insufficient; while threads will occupy a large stack space (1-8MB) by default, and the stack space size remains unchanged, which will cause overflow risk.

2. Different overheads: goroutinue creation and destruction consume very little, it is a user-state thread; while thread creation and destruction will have huge consumption, it is kernel-level interaction

3. Different scheduling switching: goroutinue switching consumes 200ns, optimized to 20ns after 1.4; while thread switching consumes 1000-15000 nanoseconds

4. Different complexity: goroutinue is simple and easy to use, M threads host N goroutinues; thread creation and exit are complex, communication between multiple threads is complex, network multiplexing is used, and the application service thread threshold is high

If you want to implement a concurrent program, you need to consider several aspects:

How to run the program code independently?

How does independent code communicate?

How to achieve data synchronization and scheduling synchronization?

This leads to several important components in Golang concurrent programming: Goroutinue, Channel, Context, Sync

Goroutine

The unit of concurrent execution in Golang is called Goroutinue, which is the Go coroutine.

How to use it is very simple. Use the go keyword to start a new Goroutinue

Sample code:

func main() {
    // Output odd numbers    printOdd := func() {
        for i := 1; i <= 10; i += 2 {
            (i)
            (100 * )
        }
    }

    // Output even numbers    printEven := func() {
        for i := 2; i <= 10; i += 2 {
            (i)
            (100 * )
        }
    }

    go printOdd()
    go printEven()

    // Blocking and waiting    ()
}

Execution results:

1 2 4 3 6 5 7 8 10 9 

We only need a go keyword to start a Goroutinue coroutine very easily. The program sleeps for 1 second in the end because the main Goroutinue (main function) needs to wait for the end of the internal Goroutinue run to end, otherwise the child Goroutinue program may be forced to stop halfway.

Randomness of scheduling

From the results, we can see that the output order of numbers is not in a certain order, because the scheduling execution of Goroutinue is random.

Concurrency scale of Goroutinue

The number of goroutinue itself is unlimited, but it will definitely be limited by the stack memory space and the resource of the operating system. You can obtain the current number of goroutinues through function (). As mentioned earlier, the initial stack memory of a Goroutinue is only 2KB, which is used to save the execution data in Goroutinue. The stack memory can be expanded, increased or reduced as needed, and a single Goroutinue can be expanded to up to 1GB.

The above method of using time sleep is too stupid. We can realize the collaborative scheduling of Goroutinue through the official provided .

It is used to wait for a set of Goroutinues to be executed. It is actually an implementation solution for a counter idea. Its core methods are:

  • Add(): Call this function to increase the number of waiting Goroutinues, atomic operations ensure concurrency security
  • Done(): Call this function to subtract a count, and atomic operations ensure concurrency security
  • Wait(): Call this function for blocking, and the blocking state will not be unblocked until all Goroutinues are completed, that is, when the counter reaches 0.

Now we remove the () of the last line of the above code. Execute again and get empty output. The reason is that the main Goroutinue ends directly, and the two sub Goroutinues have exited before they have time to execute. Let us use WaitGroup to transform it.

Sample code:

func main() {
    wg := {}
    // Output odd numbers    printOdd := func() {
        defer ()
        for i := 1; i <= 10; i += 2 {
            ("%d ", i)
            (100 * )
        }
    }
    // Output even numbers    printEven := func() {
        defer ()
        for i := 2; i <= 10; i += 2 {
            ("%d ", i)
            (100 * )
        }
    }
    (2)
    go printOdd()
    go printEven()
    // Blocking and waiting    ("waiting...")
    ()
    ("\nfinish...")
}

Execution results:

waiting...
2 1 3 4 6 5 7 8 9 10 
finish...

This simple example can more intuitively demonstrate the basic usage of waitGroup. waitGroup is suitable for a scenario where a main Goroutinue needs to wait for all other Goroutinues to end before the end is completed. It is not suitable for a scenario where the main Goroutinue needs to end and notify other Goroutinues to end.

There is a note here when using it, that is, waitGroup should not be copied and used, because the internally maintained counter cannot be modified, otherwise it will cause the leakage of Goroutinue, and the pointer type is required to be passed when passing values.

The internal structure of waitGroup

You can enter the source code to view the internal structure:

type WaitGroup struct {
   noCopy noCopy
   // 64-bit value: high 32 bits are counter, low 32 bits are waiter count.
   // 64-bit atomic operations require 64-bit alignment, but 32-bit
   // compilers only guarantee that 64-bit fields are 32-bit aligned.
   // For this reason on 32 bit architectures we need to check in state()
   // if state1 is aligned or not, and dynamically "swap" the field order if
   // needed.
   state1 uint64
   state2 uint32
}

It can be seen that it is not a complex structure, which means:

  • noCopy: Used to ensure that it will not be copied
  • state1: Taking the 64bit computer as an example, the high 32bit is the counter
  • state2: Taking the 64bit computer as an example, the low 32bit is the waiting Goroutinue

The core code of the three key functions:

func (wg *WaitGroup) Add(delta int) {
   ...
   state := atomic.AddUint64(statep, uint64(delta)<<32)
   ...
}
func (wg *WaitGroup) Done() {
   (-1)
}
func (wg *WaitGroup) Wait() {
   ...
   for {
      state := atomic.LoadUint64(statep)
      v := int32(state >> 32)
      w := uint32(state)
      if v == 0 {
         // Counter is 0, no need to wait.
         if  {
            ()
            ((wg))
         }
         return
      }
      // Increment waiters count.
      if atomic.CompareAndSwapUint64(statep, state, state+1) {
         if  && w == 0 {
            // Wait must be synchronized with the first Add.
            // Need to model this is as a write to race with the read in Add.
            // As a consequence, can do the write only for the first waiter,
            // otherwise concurrent Waits will race with each other.
            ((semap))
         }
         runtime_Semacquire(semap)
         if *statep != 0 {
            panic("sync: WaitGroup is reused before previous Wait has returned")
         }
         if  {
            ()
            ((wg))
         }
         return
      }
   }
}

This is the end of this article about the Goroutine detailed explanation of the important components that Golang cannot avoid. For more related Golang Goroutine content, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!