goroutine and scheduler GMP model

A coroutine is a lightweight thread in user mode. Goroutine is a coroutine implemented by go itself, and GMP is the scheduler model of goroutine.

1. What is collaborative process

Process is the smallest unit of system resource allocation. The creation and destruction of process are at the system resource level, so it is a relatively expensive operation. Process is preemptive scheduling, which has three states: waiting state, ready state and running state. Processes are isolated from each other. They have their own system resources, which is more secure, but there is also the problem of inconvenient communication between processes.
Thread is the smallest unit of CPU scheduling, and process is the carrier container of thread. In addition to sharing process resources, multiple threads also have a small part of their own independent resources. Therefore, compared with the process, it is lighter. The communication between multiple threads in the process is easier than the process, but it also brings the problems of synchronization and mutual exclusion and thread safety. Nevertheless, multithreaded programming is still the mainstream of server programming.
Coprocessor is a computer program component that allows pause and resume execution, so it can be used as a general non preemptive multitasking subroutine. In some data, the coordination process is called micro thread or user state lightweight thread. The coordination process scheduling does not need the participation of the kernel, but is completely determined by the user state program. Therefore, the coordination process is insensitive to the system. If the cooperative process is controlled by the user state, there is no forced CPU control switching to other incoming threads like preemptive scheduling. Multiple cooperative processes conduct cooperative scheduling. After the cooperative process actively transfers the control, other cooperative processes can be executed. In this way, the system switching overhead is avoided and the CPU efficiency is improved.

2.Goroutine introduction

Coroutine is the coroutine. Go language supports coroutine at the language level and is called goroutine. Goroutine is very simple to use. You only need to use the go keyword to start a coroutine, and it runs asynchronously. You don't need to wait for it to run and execute the subsequent code.

go func()//Start a coroutine through the go keyword to run the function

Golang implements goruntine and scheduler with two-level thread implementation model. Its advantage lies in parallelism and very low resource use.

3.GMP model

Thread implementation model
Thread implementation model is mainly divided into user level thread model, kernel level thread model and two-level thread model. Their difference lies in the correspondence between user threads and kernel threads.
GMP model
Go implements the MPG model. GPM model uses an M:N scheduler to schedule any number of coprocesses to run in any number of system threads, so as to ensure the speed of context switching and use multi-core, but increase the complexity of the scheduler.

M: Represents the real kernel OS thread and the real worker
G: Represents a goroutine, which has its own stack for scheduling.
P: Representing the context of scheduling, it can be regarded as a local scheduler to make go code run on a thread. It is the key to realize the mapping from N:1 to N:M.

An M will be associated with two things, one is the kernel thread, and the other is the executable process.
A context P has two types of goroutines, one is running, and the green G in the figure; One is queuing. The blue G in the figure will be stored in the runqueue of the process.
The number of context P here also represents the number of Goroutinue runs. Generally, if it is set to several, several will run concurrently in the machine. Of course, the number of P here can be set through the value of the environment variable GOMAXPROCS or by calling the function runtime GOMAXPROCS(), the maximum value is 256.

procs := ncpu
if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
        procs = n
}
if procresize(procs) != nil {
        throw("unknown runnable goroutine during bootstrap")
}

The simple process of the whole GPM scheduling is as follows:

The newly created Goroutine will be stored in the Global queue and wait for the Go scheduler to schedule. Then, Goroutine will be assigned to one of the logical processors P and put into the Local running queue corresponding to the logical processor, and finally wait for the execution of the logical processor P.
After M is bound with P, m will continuously take g out of P's Local queue without lock and switch to G's stack for execution. When there is no G in P's Local queue, m will obtain a G from the Global queue. When there is no G to be run in the Global queue, M will try to steal part of G from other p to perform load balancing between P.
Goroutine also has different state switching in the whole survival period, mainly including the following states:

4. Examples

func action(i int) {
   fmt.Printf("Test Goroutine %d \n", i)
}

func main() {
   for i := 0; i < 10; i++ {
      go action(i)
   }
   time.Sleep(5 * time.Millisecond)
}

The above sleep is required, otherwise you may not see the output or the output is incomplete. But there is another problem with the above writing method: if there is a panic in the execution of an action, the whole program will collapse. For example:

func action(i int) {
   if i == 3 {
      panic("panic 3")
   }
   fmt.Printf("Test Goroutine %d \n", i)
}

func main() {
   for i := 0; i < 100; i++ {
      go action(i)
   }
   time.Sleep(5 * time.Millisecond)
}

Therefore, when you are required to use goroutine, you need to use recover to catch exceptions.

func action(i int) {
   if i == 3 {
      panic("panic 3")
   }
   fmt.Printf("Test Goroutine %d \n", i)
}

func main() {
   for i := 0; i < 100; i++ {
      go func() {
         defer func() {
            if err := recover(); err != nil {
               fmt.Printf("recover panic: %+v\n", err)
            }
         }()
         action(i)
      }()
   }
   time.Sleep(5 * time.Millisecond)
}

But there is another problem like this. The i passed from go to the subprocess is similar to a kind of reference passing, so that the i output later may be repeated. The solution is to manually pass parameters to the subprocess or redefine new variables to the subprocess in for.

func action(i int) {
   if i == 3 {
      panic("panic 3")
   }
   fmt.Printf("Test Goroutine %d \n", i)
}

func main() {
   for i := 0; i < 100; i++ {
      go func(t int) {
         defer func() {
            if err := recover(); err != nil {
               fmt.Printf("recover panic: %+v\n", err)
            }
         }()
         action(t)
      }(i)
   }
   time.Sleep(5 * time.Millisecond)
}

Looking at the above code, we always use the sleep method to wait for the end of the sub process. This method is very inconvenient because you can't accurately estimate the execution time of the sub process. Therefore, go provides sync package and channel to solve the synchronization problem.

func action(i int) {
   if i == 3 {
      panic("panic 3")
   }
   fmt.Printf("Test Goroutine %d \n", i)
}

func main() {
   var wg sync.WaitGroup
   for i := 0; i < 100; i++ {
      wg.Add(1)
      go func(t int) {
         defer func() {
            if err := recover(); err != nil {
               fmt.Printf("recover panic: %+v\n", err)
            }
            wg.Done()
         }()
         action(t)
      }(i)
   }
   //time.Sleep(5 * time.Millisecond)
   wg.Wait()
}

func action(i int) {
   if i == 3 {
      panic("panic 3")
   }
   fmt.Printf("Test Goroutine %d \n", i)
}

func main() {
   count := 10
   ch := make(chan bool, count)
   for i := 0; i < count; i++ {
      go func(t int) {
         defer func() {
            if err := recover(); err != nil {
               fmt.Printf("recover panic: %+v\n", err)
            }
            ch <- true
         }()
         action(t)
      }(i)
   }
   for i := 0; i < count; i++ {
      <-ch
   }
}

Borrow go channel to realize PR

var (
   infos = make(chan int, 10)
)

// producer
func producer(index int) {
   infos <- index
}

// consumer
func consumer(index int) {
   fmt.Printf("Consumer : %d, Receive : %d\n", index, <-infos)
}

func main() {
    // Ten producers
    for index := 0; index < 10; index++ {
       go producer(index)
    }
    
    // Ten consumers
    for index := 0; index < 10; index++ {
       go consumer(index)
    }
    
    time.Sleep(5 * time.Millisecond)
}

Reference resources

goroutine
https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-goroutine/
https://segmentfault.com/a/1190000018150987
chanel
https://colobu.com/2016/04/14/Golang-Channels/

Tags: Go

Posted by jabapyth on Thu, 14 Apr 2022 22:02:41 +0930