Go concurrent scheduling advanced - circular scheduling, not the dead loop you understand

Go concurrent scheduling advanced - [gongzong No.: stack future]

original text
3. Cyclic scheduling

All GMP initialization has been completed. It's time to start the runtime scheduler. We already know that when all the preparations are completed, the last call to start execution is runtime Mstart.

Main functions of mstart:

  • Determine the boundary of the execution stack

  • Start mstart1

  • Set exit thread flag ossack = true

  • Call mexit (ossack) to exit the thread

Let's look at mstart1:

  • If the current m is not m0, bind p

  • Start calling schedule()

Therefore, we can see that the scheduling loop schedule cannot be returned, so the last mexit will not be executed at present, so all the threads created by the Go program cannot be released.

1. Binding of M and P

The binding process between M and P (acquire function calls wirep binding) simply saves the P in the P linked list to the P pointer in M. Before binding, the state of P must be_ Pidle, the state of P after binding must be_ Prunning.

 // Bind p to m, and P and M refer to each other
 _g_.m.p.set(_p_) // *_g_.m.p = _p_
 _p_.m.set(_g_.m) // *_p_.m = _g_.m

 // Modify p's status
 _p_.status = _Prunning

2. park and unpark of M

For whatever reason, the stopm call may be executed when M needs to be park. This call will park M and block it until it is unpark. This process is the worker thread's Park and unpark.

func stopm() {
 _g_ := getg()
 (...)

 // Put m back in the free list because we're going to park soon
 lock(&sched.lock)
 mput(_g_.m)
 unlock(&sched.lock)

 // park the current M, which is blocked here until it is awakened
 notesleep(&_g_.m.park)

 // Clear pending note s
 noteclear(&_g_.m.park)

 // At this time, it has been unpark, indicating that there are tasks to be executed
 // acquire P now
 acquirep(_g_.m.nextp.ptr())
 _g_.m.nextp = 0
}

Its process is also very simple. Put m back into the free list, and then use note to register a suspension notice until it is restarted.

3. Core scheduling

// Round of scheduler: find runnable Goroutine and execute it and never return
func schedule() {
 _g_ := getg()
 (...)
 top:
   if sched.gcwaiting != 0 {
    // If GC is required, no more scheduling is required
    gcstopm() //Park. Even if you steal it, you will park it
    goto top
   }
   var gp *g
   (...)
   if gp == nil {
   // Description not in GC
   // Check the global queue every 61 times to ensure fairness
   // Otherwise, two goroutines can always occupy the local runqueue by repawn each other
   if _g_.m.p.ptr().schedtick%61 == 0 && sched.runqsize > 0 {
    lock(&sched.lock)
    // Steal g from global queue
    gp = globrunqget(_g_.m.p.ptr(), 1)
    unlock(&sched.lock)
  }
 }
  
   if gp == nil {
   // Description is not in gc
   // Two cases:
   //  1. Common sampling
   //  2. Fetches that cannot be stolen from the global queue
   // Fetch from local queue
   gp, inheritTime = runqget(_g_.m.p.ptr())
   (...)
 }
  if gp == nil {
   // If you can't steal, sleep and block here
   gp, inheritTime = findrunnable()
  }

  // I must have got g by this time

  if _g_.m.spinning {
   // If m is a spin state, then
   //   1. From spin to non spin
   //   2. If there is no spin state m, create a new spin state M
   resetspinning()
  }
   
  if gp.lockedm != 0 {
     // If g needs to lock to m, the current p
     // Give this g to lock
     // Then block and wait for a new p
     startlockedm(gp)
     goto top
  }

  // Start execution
  execute(gp, inheritTime)
}

Take a look at execute:

func execute(gp *g, inheritTime bool) {
 _g_ := getg()

 // Switch g to_ Running status
 casgstatus(gp, _Grunnable, _Grunning)
  
 // M and G bind to start execution
 _g_.m.curg = gp
 gp.m = _g_.m

 // G finally began to execute
 gogo(&gp.sched)
}

When execute is started, G will be switched to_ Running status, binding m and g at the same time. Finally, gogo is called to start execution.

The implementation of gogo is a piece of assembly code, which is obscure and difficult to understand. But the general meaning is to complete the stack switching from g0 to gp, and then start executing runtime Main function or user-defined goroutine task.

After execution, main goroutine directly calls eixt(0) to exit, while ordinary goroutine calls goexit - > goexit1 - > MCALL to clean up after ordinary goroutine exits, then switches to g0 stack, calls goexit0 function, adds ordinary goroutine to cache pool, and then calls schedule function for a new round of scheduling.

The scheduling is too complicated. The general process is as follows:

schedule() -> execute() -> gogo() -> goroutine task -> goexit() -> goexit1() -> mcall() -> goexit0() -> schedule()

It can be seen that a round of scheduling starts from calling the schedule function and calls the schedule function again after a series of processes to schedule a new round. The process from one round of scheduling to a new round of scheduling is called a scheduling cycle.

The scheduling cycle here refers to the scheduling cycle of a working thread, while there are multiple working threads in the same Go program, and each working thread is carrying out its own scheduling cycle.

4. Summary

Because the scheduling core is too complex, we only need to understand the general process or ideas, and there is no need to go deep into the underlying details. Because the deeper you get, the more difficult it is for you to understand some details. I give up here and don't want to study it in depth. Just know that this circular scheduling is not a dead cycle, and each link in the scheduling chain should also be roughly understood. Well, that's all for today. Welcome to pay attention to forwarding and sharing.

Gongzong No.: future

So that many coder s in the confused stage can find light from here, stack creation, contribute to the present and benefit the future

Keywords: Go Interview

Added by Azu on Sun, 09 Jan 2022 07:02:47 +0200