0. Introduction
In the previous blog—"Golang Scheduler (4)—goroutine Scheduler"There has been an unanswered question left in this: If a G executes too long, how can other Gs be scheduled normally, which leads to the next topic:Collaboration and preemption。
In Gov1.2
The version implements preemptive calls based on collaboration. The basic principle of this call is:
- when
sysmon
If the monitoring thread finds that the execution time of a coroutine is too long, it will friendlyly set preemption marks for the coroutine; - When this coroutine
Call
When a function is used, it will check whether the stack is expanded, and here it will check for preemption marks. If they are marked, the CPU will be given up to achieve scheduling.
However, this scheduling method is proactive and based on collaboration, but it cannot face some scenarios. For example, if no call occurs in the dead loop, then the coroutine will be executed forever and scheduling will never occur, which is obviously unacceptable.
So, inv1.14
In version, Go finally introduced signal-based preemption scheduling. Below, we will introduce these two preemption scheduling.
1. The user takes the initiative to give up the CPU: function
Before introducing the two preemption scheduling, let's first introducefunction:
// Gosched yields the processor, allowing other goroutines to run. It does not // suspend the current goroutine, so execution resumes automatically. func Gosched() { checkTimeouts() mcall(gosched_m) }
According to the instructions,The function will actively abandon the current processor and allow other coroutines to execute, but will not pause itself, but will only transfer the scheduling rights, and then rely on the scheduler to obtain rescheduling.
After that, it will passmcall
Switch function tog0
To execute the stackgosched_m
function:
// Gosched continuation on g0. func gosched_m(gp *g) { if { traceGoSched() } goschedImpl(gp) }
gosched_m
CallgoschedImpl
function, which will be coroutinegp
Transfer this M andgp
Put it in the global queue and wait for scheduling.
func goschedImpl(gp *g) { status := readgstatus(gp) if status&^_Gscan != _Grunning { dumpgstatus(gp) throw("bad g status") } casgstatus(gp, _Grunning, _Grunnable) dropg() // Make the current m give up gp, which is its parameter curg lock(&) globrunqput(gp) // And put gp into the global queue and wait for scheduling unlock(&) schedule() }
AlthoughIt has the ability to actively give up the CPU, but it has high requirements for users and is not user-friendly.
2. Preemptive scheduling based on collaboration
2.1 Scenario
package main import ( "fmt" "runtime" "sync" "time" ) var once = {} func f() { (func() { ("I am go routine 1!") }) } func main() { defer ((1)) go func() { for { f() } }() (10 * ) ("I am main goroutine!") }
We consider the above code. First, we set the number of P to 1, and then start a coroutine to enter a dead loop, and call a function in a loop. If the dispatch is not preempted, the coroutine will always occupy P, that is, it will always occupy the CPU, and the code will never be executed("I am main goroutine!")
This is the job. Let’s take a look at how collaborative preemption avoids the above problems.
2.2 Stack expansion and preemption mark
$ go tool compile -N -l
$ go tool objdump >>
Through the above instructions, we obtain the assembly code of the code in 2.1 and intercept thef
The assembly code of the function is as follows:
TEXT "".f(SB) gofile../home/chenyiguo/smb_share/go_routine_test/
:12 0x151a 493b6610 CMPQ 0x10(R14), SP
:12 0x151e 762b JBE 0x154b
:12 0x1520 4883ec18 SUBQ $0x18, SP
:12 0x1524 48896c2410 MOVQ BP, 0x10(SP)
:12 0x1529 488d6c2410 LEAQ 0x10(SP), BP
:13 0x152e 488d0500000000 LEAQ 0(IP), AX [3:7]R_PCREL:"".once
:13 0x1535 488d1d00000000 LEAQ 0(IP), BX [3:7]R_PCREL:"".f.func1·f
:13 0x153c e800000000 CALL 0x1541 [1:5]R_CALL:sync.(*Once).Do
:16 0x1541 488b6c2410 MOVQ 0x10(SP), BP
:16 0x1546 4883c418 ADDQ $0x18, SP
:16 0x154a c3 RET
:12 0x154b e800000000 CALL 0x1550 [1:5]R_CALL:runtime.morestack_noctxt
:12 0x1550 ebc8 JMP "".f(SB)
The first line,CMPQ 0x10(R14), SP
Just comparisonSP
and0x10(R14)
(Actuallystackguard0
) size (noteAT&T
In formatCMP
The order of series instructions) whenSP
Less than or equal to0x10(R14)
When it is transferred to0x154b
Address callruntime.morestack_noctxt
, trigger the stack expansion operation. In fact, if you observe carefully, you will find that all the functions' prologues (the front of the function call) are inserted into the detection instructions unless marked on the function.//go:nosplit
。
Next, we will focus on two points to open up the entire link, namely:
- How to reschedule stack expansion and give up the CPU's execution rights?
- When will the stack expansion mark be set?
2.3 How to trigger rescheduling when stack expansion
// morestack but not preserving ctxt. TEXT runtime·morestack_noctxt(SB),NOSPLIT,$0 MOVL $0, DX JMP runtime·morestack(SB) TEXT runtime·morestack(SB),NOSPLIT,$0-0 ... // Set g->sched to context in f. MOVQ 0(SP), AX // f's PC MOVQ AX, (g_sched+gobuf_pc)(SI) LEAQ 8(SP), AX // f's SP MOVQ AX, (g_sched+gobuf_sp)(SI) MOVQ BP, (g_sched+gobuf_bp)(SI) MOVQ DX, (g_sched+gobuf_ctxt)(SI) ... CALL runtime·newstack(SB) CALL runtime·abort(SB) // crash if newstack returns RET
In the above code,runtime·morestack_noctxt
Callruntime·morestack
,existruntime·morestack
In the coroutine, the PC and SP of the coroutine will be recorded first, and then the:
func newstack() { ... gp := ... stackguard0 := (&gp.stackguard0) ... preempt := stackguard0 == stackPreempt ... if preempt { if gp == .g0 { throw("runtime: preempt g0") } if == 0 && == 0 { throw("runtime: g is running but p is not") } if { // We're at a synchronous safe point now, so // do the pending stack shrink. = false shrinkstack(gp) } if { preemptPark(gp) // never returns } // Act like goroutine called . gopreempt_m(gp) // never return } ... }
We simplifyFunctions, summarizing it, are through existing work coroutines
stackguard0
Fields to determine whether preemption should occur. If necessary, callgopreempt_m(gp)
function:
func gopreempt_m(gp *g) { if { traceGoPreempt() } goschedImpl(gp) }
You can see,gopreempt_m
Functions and mentioned earlierGosched
What is mentioned in the functiongosched_m
Like functions, they will be calledgoschedImpl
Function, for coroutinesgp
Transfer this M andgp
Put it in the global queue and wait for scheduling.
Here we understand that once stack expansion occurs, there is a possibility of transferring the execution rights and rescheduling. So when will stack expansion occur?
2.4 When to set the stack expansion mark
In the code,stackguard0
Set field tostackPreempt
There are many places, but the one that matches our above scenario is still the background monitoring threadsysmon
Loop, for those stuck in system calls and long-runninggoroutine
The right to run is seizedretake
function:
func sysmon() { ... for { ... // retake P's blocked in syscalls // and preempt long running G's if retake(now) != 0 { idle = 0 } else { idle++ } ... } }
func retake(now int64) uint32 { ... for i := 0; i < len(allp); i++ { ... s := _p_.status sysretake := false if s == _Prunning || s == _Psyscall { // Preempt G if it's running for too long. t := int64(_p_.schedtick) if int64() != t { = uint32(t) = now } else if +forcePreemptNS <= now { // forcePreemptNS=10ms preemptone(_p_) // Set the stack expansion mark here // In case of syscall, preemptone() doesn't // work, because there is no M wired to P. sysretake = true } } ... } unlock(&allpLock) return uint32(n) }
Among them, inpreemptone
Settings for stack expansion marks in the function:
func preemptone(_p_ *p) bool { mp := _p_.() if mp == nil || mp == getg().m { return false } gp := if gp == nil || gp == mp.g0 { return false } = true // Every call in a goroutine checks for stack overflow by // comparing the current stack pointer to gp->stackguard0. // Setting gp->stackguard0 to StackPreempt folds // preemption into the normal stack overflow check. gp.stackguard0 = stackPreempt // Set stack expansion mark // Request an async preemption of this P. if preemptMSupported && == 0 { _p_.preempt = true preemptM(mp) } return true }
Through the above, we colludedgoroutine
The logic of collaborative preemption:
- First of all, the background monitoring thread will run too long (
≥10ms
) coroutine sets stack expansion marks; - When a coroutine runs to the prologue of any function, it will first check the stack expansion mark;
- If stack expansion is required, the operation rights of this coroutine will be seized when stack expansion is performed, thereby realizing preemptive scheduling.
3. Signal-based preemptive scheduling
Analyzing the above conclusions, we can know that the above preemption trigger logic has a fatal disadvantage, that is, it must be run to the preamble part of the function stack, and this cannot read the running rights of the following coroutines. Before Go version 1.14, the code will not print the last sentence."I am main goroutine!"
:
package main import ( "fmt" "runtime" "sync" "time" ) var once = {} func main() { defer ((1)) go func() { for { (func() { ("I am go routine 1!") }) } }() (10 * ) ("I am main goroutine!") }
Because in the above coroutinefor
A loop is a dead loop and does not contain stack expansion logic, so its own execution rights will not be transferred.
3.1 Send preemption signal
to this end,Go SDK
Signal-based preemptive scheduling is introduced. We pay attention to the analysis of the previous sectionpreemptone
The function code has the following parts:
if preemptMSupported && == 0 { _p_.preempt = true preemptM(mp) }
inpreemptM
The function will be sent_SIGURG
Signals to threads that need to be preempted:
const sigPreempt = _SIGURG func preemptM(mp *m) { // On Darwin, don't try to preempt threads during exec. // Issue #41702. if GOOS == "darwin" || GOOS == "ios" { () } if (&, 0, 1) { if GOOS == "darwin" || GOOS == "ios" { (&pendingPreemptSignals, 1) } // If multiple threads are preempting the same M, it may send many // signals to the same M such that it hardly make progress, causing // live-lock problem. Apparently this could happen on darwin. See // issue #37741. // Only send a signal if there isn't already one pending. signalM(mp, sigPreempt) } if GOOS == "darwin" || GOOS == "ios" { () } }
3.2 Preemption call injection
Speaking of this, we need to go back to the beginning, in the first coroutinem0
Openmstart
On the calling link, it will be calledmstartm0
Functions will be called hereinitsig
:
func initsig(preinit bool) { ... for i := uint32(0); i < _NSIG; i++ { ... handlingSig[i] = 1 setsig(i, (sighandler)) } }
In the above, registeredsighandler
function:
func sighandler(sig uint32, info *siginfo, ctxt , gp *g) { ... if sig == sigPreempt && == 0 { // Might be a preemption signal. doSigPreempt(gp, c) // Even if this was definitely a preemption signal, it // may have been coalesced with another signal, so we // still let it through to the application. } ... }
Then receivesigPreempt
When the signal is passeddoSigPreempt
Function processing is as follows:
func doSigPreempt(gp *g, ctxt *sigctxt) { // Check if this G wants to be preempted and is safe to // preempt. if wantAsyncPreempt(gp) { if ok, newpc := isAsyncSafePoint(gp, (), (), ()); ok { // Adjust the PC and inject a call to asyncPreempt. (abi.FuncPCABI0(asyncPreempt), newpc) // Insert preemption call } } // Acknowledge the preemption. (&, 1) (&, 0) if GOOS == "darwin" || GOOS == "ios" { (&pendingPreemptSignals, -1) } }
final,doSigPreempt—>asyncPreempt->asyncPreempt2
:
func asyncPreempt2() { gp := getg() = true if { mcall(preemptPark) } else { mcall(gopreempt_m) } = false }
Then, we're back to what we're familiar withgopreempt_m
Functions, I won't go into details here.
Therefore, for signal-based preemption scheduling, the summary is as follows:
- M1 sends a signal
_SIGURG
; - M2 receives the signal and processes it through the signal processing function;
- M2 modifies the execution context and restores to the modified location;
- Re-enter the scheduling loop and then schedule other
goroutine
。
4. Summary
Overall,Go
The development of scheduling strategies is also gradually developing with the enrichment of demand. Collaborative scheduling can ensure that users with function calls can stop normally; preemptive scheduling can avoid garbage collection delays at any time caused by the dead loop.
This is the end of this article about the detailed explanation of collaboration and preemption of Go scheduler learning. For more related Go scheduler content, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!