This is a creation in Article, where the information may have evolved or changed.
Status overview
When explaining the part of the operating system process scheduling, almost all books will first list a process of the state migration diagram, through the state diagram, can be very clear the process of scheduling each link together, easy to understand.
The Go Runtime Scheduler can actually be seen as a simplified version of the OS Scheduler, a goroutine in its life cycle, and also contains transformations of various states. Figuring out the principle of switching between these States and States is very helpful in figuring out the entire go scheduler.
Here is a summary of Goroutine's state migration diagram, a circular box indicating the state, arrows and text messages indicating the direction and condition of the switch:
State details
The following is a simple analysis, where the state gidle in the Go Scheduler code is not really used, so directly ignored. In fact, once the runtime creates a new goroutine structure, it will set its state to grunnable and join the task queue, so we'll start with that state as a starting point.
Grunnable
In Golang, a co-process is set to the Grunnable state in the following cases:
Create
In the Go language, all tasks, including the execution goroutine of the user entry function main main, are created by the two functions, runtime Newproc-runtime Newproc1, which is actually a layer of encapsulation of the latter. Provides variable parameter support, and the GO keyword of the go language will eventually be mapped to a call to runtime Newproc by the compiler. When runtime Newproc1 completes the allocation and initialization of the resource, the status of the new task is set to Grunnable, which is then added to the current P's private task queue, waiting for the dispatch to execute. The relevant initialization code is as follows:
G* runtime·newproc1(FuncVal *fn, byte *argp, int32 narg, int32 nret, void *callerpc) { G *newg; P *p; int32 siz; ...... // 获取当前g所在的p,从p中创建一个新g(newg) p = g->m->p; if((newg = gfget(p)) == nil) { ...... } ...... // 设置Goroutine状态为Grunnable runtime·casgstatus(newg, Gdead, Grunnable); ..... // 新创建的g添加到run队列中 runqput(p, newg); ......}
Blocking Task Wakeup
When a blocking task (with a status of Gwaiting) is awakened when a wait condition is met-such as a task g#1 writing data to a channel, the task that waits to read the channel data is awakened before g#2--g#1 by calling runtime ready g# The 2 status is reset to Grunnable and added to the task queue. About task blocking, which is described in more detail later.
// Mark gp ready to run.voidruntime·ready(G *gp){ uint32 status; status = runtime·readgstatus(gp); // Mark runnable. g->m->locks++; if((status&~Gscan) != Gwaiting){ dumpgstatus(gp); runtime·throw("bad g->status in ready"); } // 设置被唤醒的g状态从Gwaiting转变至Grunnable runtime·casgstatus(gp, Gwaiting, Grunnable); // 添加到运行队列中 runqput(g->m->p, gp); if(runtime·atomicload(&runtime·sched.npidle) != 0 && runtime·atomicload(&runtime·sched.nmspinning) == 0) // 看起来这是个比较重要的函数,但还不是很理解 wakep(); g->m->locks--; if(g->m->locks == 0 && g->preempt) g->stackguard0 = StackPreempt;}
Other
Another path is the transition from grunning and Gsyscall states to grunnable, and we are all merging to the later introduction. In short, the task in grunnable must be in a task queue, waiting to be scheduled for execution at any time.
Grunning
All tasks that have a state of grunnable can be obtained through the findrunnable function by the Scheduler (P&M), which then switches its state to grunning through the Execute function, and finally calls runtime Gogo to load its context and execute.
One round of Scheduler:find a runnable goroutine and execute it. Never returns.static voidschedule (void) {G *gp; UInt32 tick; if (g->m->locks) runtime throw ("schedule:holding locks"); if (G->M->LOCKEDG) {stoplockedm (); Execute (G->M->LOCKEDG); Never returns. }top:if (runtime sched.gcwaiting) {gcstopm (); Goto top; } GP = nil; Pick a running g and execute ... if (gp = = nil) {GP = Findrunnable (); Blocks until work is available resetspinning (); } ... execute (GP);} Schedules GP to run in the current m.//never returns.static Voidexecute (G *gp) {int32 Hz; The state changed from grunnable to Grunning runtime Casgstatus (GP, Grunnable, grunning); gp->waitsince = 0; Gp->preempt = false; gp->stackguard0 = Gp->stack.lo + Stackguard; g->m->p->schedtick++; G->m->curg = GP; Gp->m = g->m; Check whether the profiler needs to being turned on orOff. Hz = Runtime sched.profilehz; if (g->m->profilehz! = Hz) Runtime Resetcpuprofiler (Hz); Real execution G runtime Gogo (&gp->sched);}
The Go essence uses a cooperative scheduling scheme, a running task that needs to explicitly yield the processor by invoking yields, and after Go1.2, the runtime also begins to support a certain level of task preemption-- When the system thread Sysmon discovers that a task is executing too long or runtime determines that it needs to be garbage collected, the task is set to "can be preempted", and when the task next function call is made, the processor is conceded and re-cut to the grunnable state. The implementation details of the preemption mechanism in Go1.2, followed by a chance to introduce.
Gsyscall
To ensure high concurrency performance, the go runtime calls the runtime Entersyscall function to set its state to gsyscall--before the task executes the OS system call, if the system call is blocked or executed too long, the current m and P are separated-- When the system call returns, the execution thread calls runtime Exitsyscall to try to regain p, and if successful and the current task is not preempted, the state is tangent back to grunning and continues execution; otherwise the state is set to Grunnable, waiting to be scheduled for execution again.
// Puts the current goroutine into a waiting state and calls unlockf.// If unlockf returns false, the goroutine is resumed.voidruntime·park(bool(*unlockf)(G*, void*), void *lock, String reason){ void (*fn)(G*); g->m->waitlock = lock; g->m->waitunlockf = unlockf; g->waitreason = reason; fn = runtime·park_m; runtime·mcall(&fn);}// runtime·park continuation on g0.voidruntime·park_m(G *gp){ bool ok; // 设置当前状态从Grunning-->Gwaiting runtime·casgstatus(gp, Grunning, Gwaiting); // 当前g放弃m dropg(); if(g->m->waitunlockf) { ok = g->m->waitunlockf(gp, g->m->waitlock); g->m->waitunlockf = nil; g->m->waitlock = nil; if(!ok) { runtime·casgstatus(gp, Gwaiting, Grunnable); execute(gp); // Schedule it back, never returns. } } schedule();}
Gwaiting
When a task requires a resource or running condition that cannot be met, the runtime Park function needs to be called into the state, and the task will remain in wait state until the wait condition is met. In addition to the channel examples previously cited, the Go language timer, network IO operations can cause task blocking.
// runtime·park continuation on g0.voidruntime·park_m(G *gp){ bool ok; runtime·casgstatus(gp, Grunning, Gwaiting); dropg(); if(g->m->waitunlockf) { ok = g->m->waitunlockf(gp, g->m->waitlock); g->m->waitunlockf = nil; g->m->waitlock = nil; if(!ok) { runtime·casgstatus(gp, Gwaiting, Grunnable); execute(gp); // Schedule it back, never returns. } } schedule();}
The Runtime Park function contains 3 parameters, the first is the Unlock function pointer, the second is a lock pointer, and finally a string that describes the cause of the blocking. It is clear that the first two parameters are paired structures-because some lock may be obtained before the task is blocked, the lock must be freed after the task state is saved to avoid data contention. We know that the channel must secure mutually exclusive access through lock, a blocking task g#1 need to put itself in the channel's waiting queue, if the lock is released before the completion context is saved, it may cause g#2 to g#1 the unknown state to grunnable. So release lock must be done within Runtime Park. Because the lock type is different for a task when blocking--such as the lock for a select operation is actually a set of locks--you need to specifically indicate how the unlock is done. The last parameter is to conveniently discover the cause of the task blocking during GDB debugging. By the way, when all the tasks are in the gwaiting state, it means that the current program has entered a deadlock state, it is impossible to continue, then runtime will detect this situation, and output all gwaiting task BackTrace information.
Gdead
Finally, when a task executes, it calls runtime goexit to end its own life-the state is set to Gdead, and the struct is chained to a free G-linked list that belongs to the current p for later use.
The concurrency model of Go language basically follows the CSP model, the goroutine between the channel communication, there is no wait mechanism like the UNIX process waiting or waitpid, and there is no convergence mechanism like the pthread_join in POSIX Thread. There are no interrupt mechanisms such as kill or signal. Each goroutine after the end of their own exit destruction, leaving no trace.