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