调度器

golang语言强大的并发编程能力,得益于语言在原生层面对并发的支持。接下来介绍go语言运行时调度器的设计和实现,以及运行调度相关的数据结构。

进程与线程 process/thread

多个线程可以同属于一个进程,并共享内存空间。线程之间的通信就是通过基于共享的内存进行的,与重量的进程相比,线程更轻。但是每个线程仍会占用1M以上的内存空间,进行线程间切换时,不仅需要多申请内存,同时恢复寄存器的内容是也会向操作系统额外申请资源。每个线程上下文的切换大概需要消耗1us的时间,而goroutine切换大概只需要0.2us,同时每个goroutine只占用2k内存。

调度器的设计原理

golang-gmp

GMP模型 G-Goroutine M-线程 P-中间层

P的数据结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
struct P {
	Lock;

	uint32	status;
	P*	link;
	uint32	tick;
	M*	m;						//线程M
	MCache*	mcache;

	G**	runq;					//可运行的goroutine组成的环形队列
	int32	runqhead;
	int32	runqtail;
	int32	runqsize;

	G*	gfree;
	int32	gfreecnt;
};
抢占式调度器

基于工作窃取的多线程调度器将每一个线程都绑定在独立的CPU上,这些线程会被不同处理器管理,不同的处理器通过工作窃取对任务进行再分配实现任务的平衡,也能提升调度器和Go语言程序的整体性能。

基于协作的抢占式调度器发展到基于信号的抢占式调度器

  • 基于协作的抢占式调度器工作原理
    1. 编译器会在调用函数前插入runtime.morestack
    2. Go语言在运行时会在垃圾回收暂停时,系统监控发现Goroutine运行超过10ms时发出抢占请求StackPreempt。系统为 Goroutine 引入 stackguard0 字段,该字段被设置成 StackPreempt 意味着当前 Goroutine 发出了抢占请求
    3. 当发生系统调用时,会执行编译器插入的runtime.morestack ,它调用的runtime.newstack 会检查goroutine的stackguard0字段是否为StackPreempt。
    4. 如果为StackPreempt,则会触发抢占,让出线程。
  • 基于信号的抢占式调度器
    1. 是一种非协作的抢占
    2. 程序启动时,在 runtime.sighandler 中注册 SIGURG 信号的处理函数 runtime.doSigPreempt
    3. 在触发垃圾回收的栈扫描时会调用runtime.suspendG挂起 Goroutine,该函数会执行下面的逻辑:
      1. _Grunning 状态的 Goroutine 标记成可以被抢占,即将 preemptStop 设置成 true
      2. 调用 runtime.preemptM 触发抢占;
    4. runtime.preemptM 会调用 runtime.signalM 向线程发送信号 SIGURG
    5. 操作系统会中断正在运行的线程并执行预先注册的信号处理函数 runtime.doSigPreempt
    6. runtime.doSigPreempt 函数会处理抢占信号,获取当前的 SP 和 PC 寄存器并调用 runtime.sigctxt.pushCall
    7. runtime.sigctxt.pushCall 会修改寄存器并在程序回到用户态时执行 runtime.asyncPreempt
    8. 汇编指令 runtime.asyncPreempt 会调用运行时函数 runtime.asyncPreempt2
    9. runtime.asyncPreempt2 会调用 runtime.preemptPark
    10. runtime.preemptPark 会修改当前 Goroutine 的状态到 _Gpreempted 并调用 runtime.schedule 让当前函数陷入休眠并让出线程,调度器会选择其它的 Goroutine 继续执行;

数据结构

golang-scheduler

G-Goroutine,指一个待执行的任务

M-操作系统线程

P-运行在线程上的本地调度器

G

goroutine是Go语言调度器中待执行的任务,它在运行时调度器中的地位同线程在操作系统中差不多,但是占用更少的内存,降低上下文切换的开销。goroutine是由runtime管理的,是go语言在用户态的线程。

runtime.g表示goroutine的结构体

M

M是操作系统线程,最多只能有GOMAXPROCS个活跃线程能正常运行。

scheduler-m-and-thread

runtime.m

1
2
3
4
5
type m struct {
	g0   *g
	curg *g
	...
}

g0是持有调度栈的goroutine,curg是在当前线程上运行的用户goroutine,操作系统只关心这两个goroutine。

g0-and-g

g0是一个运行中比较特殊的goroutine,深度参与运行时的调度过程,包括goroutine的创建,大内存分配和CGO的执行。

P

P是线程和goroutine的中间层,能提供线程所需要的上下文环境,负责调度线程上的等待队列,通过P的调度,那个内核线程都能够执行多个goroutine,能在goroutine进行I/O操作时及时让出计算资源,提高线程利用率。

runtime.p

Reference

https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-goroutine/