【译文】 原文地址
所有在Go中创建的goroutines都由一个内部调度程序的管理.Go调度程序试图给所有的goroutines分配运行时间,并且在当前goroutine被阻塞或终止情况下也能使CPU忙于运行其他goroutines.
Go通过GOMAXPROCS变量来限制操作系统线程同时运行的数量.这意味着,Go必须在每个正在运行的线程上调度和管理所有的goroutines.这个角色通过一个特殊的goroutine来完成,称为g0,这是为每个操作系统线程创建的第一个goroutine:
当goroutine被阻塞在channel上时,当前的goroutine就会被挂起,即处于等待模式将不会推入任何goroutines队列中.
收到消息的goroutine将切换到g0,然后将挂起的goroutine放入到本地调度队列中:
尽管g0这个特殊goroutine是管理调度的,但是它不止这些工作还有其他更多的功能.
与普通goroutine相反,g0有固定且比较大的栈.这允许Go在需要更大栈时,还能执行操作.g0的职责可以如下:
设置可同时执行的逻辑Cpu数量,默认和硬件的线程数一致而不是核心数,可以通过调用GOMAXPROCS(-1)来获取当前逻辑Cpu数最好在main函数之前设置它,GOMAXPROCS同时也是go的环境变量之一.
golang学习笔记
频繁创建线程会造成不必要的开销,所以才有了线程池.在线程池中预先保存一定数量的线程,新任务发布到任务队列,线程池中的线程不断地从任务队列中取出任务并执行,可以有效的减少创建和销毁带来的开销.
过多的线程会导致争抢cpu资源,且上下文的切换的开销变大.而工作在用户态的协程能大大减少上下文切换的开销.协程调度器把可运行的协程逐个调度到线程中执行,同时即时把阻塞的协程调度出协程,从而有效地避免了线程的频繁切换,达到了少量线程实现高并发的效果.
多个协程分享操作系统分给线程的时间片,从而达到充分利用CPU的目的,协程调度器决定了则决定了协程运行的顺序.每个线程同一时刻只能运行一个协程.
go调度模型包含三个实体:
每个处理器维护者一个协程G的队列,处理器依次将协程G调度到M中执行.
每个P会周期性地查看全局队列中是否有G待运行并将其调度到M中执行,全局队列中的G主要来自系统调用中恢复的G.
如果协程发起系统调用,则整个工作线程M被阻塞,协程队列中的其他协程都会阻塞.
当G1结束系统调用后,根据M1是否获取到P,进行不用的处理.
多个处理P维护队列可能不均衡,导致部分处理器非常繁忙,而其余相对空闲.产生原因是有些协程自身不断地派生协程.
为此Go调度器提供了工作量窃取策略,当某个处理器P没有需要调度的协程时,将从其他处理中偷取协程,每次偷取一半.
抢占式调度,是指避免某个协程长时间执行,而阻碍其他协程被调度的机制.
调度器监控每个协程执行时间,一旦执行时间过长且有其他协程等待,会把协程暂停,转而调度等待的协程,以达到类似时间片轮转的效果.比如for循环会一直占用执行权.
在IO密集型应用,GOMAXPROCS大小设置大一些,获取性能会更好.
IO密集型会经常发生系统调用,会有一个新的M启用或创建,但由于Go调度器检测M到被阻塞有一定延迟.如果P数量多,则P管理协程队列会变小.
Goroutine调度是一个很复杂的机制,下面尝试用简单的语言描述一下Goroutine调度机制,想要对其有更深入的了解可以去研读一下源码.
首先介绍一下GMP什么意思:
G ----------- goroutine: 即Go协程,每个go关键字都会创建一个协程.
M ---------- thread内核级线程,所有的G都要放在M上才能运行.
P ----------- processor处理器,调度G到M上,其维护了一个队列,存储了所有需要它来调度的G.
Goroutine 调度器P和 OS 调度器是通过 M 结合起来的,每个 M 都代表了 1 个内核线程,OS 调度器负责把内核线程分配到 CPU 的核上执行
模型图:
避免频繁的创建、销毁线程,而是对线程的复用.
①.)work stealing机制
当本线程无可运行的G时,尝试从其他线程绑定的P偷取G,而不是销毁线程.
如果有空闲的P,则获取一个P,继续执行G0.
如果没有空闲的P,则将G0放入全局队列,等待被其他的P调度.然后M0将进入缓存池睡眠.
如下图
GOMAXPROCS设置P的数量,最多有GOMAXPROCS个线程分布在多个CPU上同时运行
在Go中一个goroutine最多占用CPU 10ms,防止其他goroutine被饿死.
具体可以去看另一篇文章
【Golang详解】go语言调度机制 抢占式调度
当创建一个新的G之后优先加入本地队列,如果本地队列满了,会将本地队列的G移动到全局队列里面,当M执行work stealing从其他P偷不到G时,它可以从全局G队列获取G.
协程经历过程
我们创建一个协程 go func()经历过程如下图:
说明:
G只能运行在M中,一个M必须持有一个P,M与P是1:1的关系.M会从P的本地队列弹出一个可执行状态的G来执行,如果P的本地队列为空,就会想其他的MP组合偷取一个可执行的G来执行;
一个M调度G执行的过程是一个循环机制;会一直从本地队列或全局队列中获取G
上面说到P的个数默认等于CPU核数,每个M必须持有一个P才可以执行G,一般情况下M的个数会略大于P的个数,这多出来的M将会在G产生系统调用时发挥作用.类似线程池,Go也提供一个M的池子,需要时从池子中获取,用完放回池子,不够用时就再创建一个.
work-stealing调度算法:当M执行完了当前P的本地队列队列里的所有G后,P也不会就这么在那躺尸啥都不干,它会先尝试从全局队列队列寻找G来执行,如果全局队列为空,它会随机挑选另外一个P,从它的队列里中拿走一半的G到自己的队列中执行.
如果一切正常,调度器会以上述的那种方式顺畅地运行,但这个世界没这么美好,总有意外发生,以下分析goroutine在两种例外情况下的行为.
Go runtime会在下面的goroutine被阻塞的情况下运行另外一个goroutine:
用户态阻塞/唤醒
系统调用阻塞
当M执行某一个G时候如果发生了阻塞操作,M会阻塞,如果当前有一些G在执行,调度器会把这个线程M从P中摘除,然后再创建一个新的操作系统的线程(如果有空闲的线程可用就复用空闲线程)来服务于这个P.当M系统调用结束时候,这个G会尝试获取一个空闲的P执行,并放入到这个P的本地队列.如果获取不到P,那么这个线程M变成休眠状态, 加入到空闲线程中,然后这个G会被放入全局队列中.
队列轮转
可见每个P维护着一个包含G的队列,不考虑G进入系统调用或IO操作的情况下,P周期性的将G调度到M中执行,执行一小段时间,将上下文保存下来,然后将G放到队列尾部,然后从队列中重新取出一个G进行调度.
M0
M0是启动程序后的编号为0的主线程,这个M对应的实例会在全局变量rutime.m0中,不需要在heap上分配,M0负责执行初始化操作和启动第一个G,在之后M0就和其他的M一样了
G0
G0是每次启动一个M都会第一个创建的goroutine,G0仅用于负责调度G,G0不指向任何可执行的函数,每个M都会有一个自己的G0,在调度或系统调用时会使用G0的栈空间,全局变量的G0是M0的G0
一个G由于调度被中断,此后如何恢复?
中断的时候将寄存器里的栈信息,保存到自己的G对象里面.当再次轮到自己执行时,将自己保存的栈信息复制到寄存器里面,这样就接着上次之后运行了.
我这里只是根据自己的理解进行了简单的介绍,想要详细了解有关GMP的底层原理可以去看Go调度器 G-P-M 模型的设计者的文档或直接看源码
参考: ()
()
//没问题的,可以make,应该是你的主线程执行完直接退出了,导致协程没机会执行,所以
//你看不到输出而已.
func?main()?{
fmt.Println("main?len:",len(pix))
go?func()?{
fmt.Println("go?len:",len(pix))
}()
time.Sleep(time.Second)
}