- 默认情况下,所有的goroutine都在同一个原生线程里跑,也就是只使用了一个CPU核。但是,通过runtime.GOMAXPROCS(4)设定,可以将goroutine调度到多个CPU上运行。
- 在同一个原生线程里,若当前goroutine不发生阻塞,那么不会主动让出CPU给其他同一线程的goroutine的。在go程序启动时,会首先创建一个特殊的内核线程sysmom,负责监控和调度。
三类对象:
- M代表线程
- P代表处理器,每一个运行的M(线程)都必须绑定一个P(处理器)
- G代表goroutine,每次使用go关键字的时候,都会创建一个G对象
图解:
当前有两个P,各自绑定了一个M,每个P上挂了一个本地goroutine队列,也有一个全局goroutine队列。流程:
- 每次使用go关键字声明时,一个G对象被创建并加入到本地G队列或者全局G队列。
- 检查是否有空闲的P(处理器),若有那么创建一个M(若有正在sleep的M那么直接唤醒它)与其绑定,然后这个M循环执行goroutine任务。
- G任务执行的顺序是,先从本地队列中找。但若某个M(线程)发现本地队列为空,那么会从全局队列中截取goroutine来执行(一次性转移(全局队列的G个数/P个数))。如果全局队列也空,那么会随机从别的P那里截取【一半】的goroutine过来(偷窃任务),若所有的P的队列都为空,那么该M(线程)就会陷入sleep。
三种调度点
如果一个goroutine运行到一个“调度点”,上下文便从队列中取出一个goroutine,开始运行新的goroutine,下面是三种调度点:
- 调用runtime.gosched函数。goroutine主动放弃CPU,该goroutine会被置为runnable状态,然后放入全局G队列,P继续执行下一个goroutine。主动行为,使用场景:一般发生在执行长任务又想其他goroutine得到执行机会时调用。
- 调用runtime.park函数。goroutine进入wait状态,除非对其调用runtime.ready函数,否则该goroutine永远不刽得到执行。而P继续执行下一个G任务。使用场景:读写channel。一般是在某个条件如果得不到满足就不能继续运行下去时调用,当条件满足后需要使用runtime.ready唤醒它,类似于Java里的await和notify
- 慢系统调用。将该处理器P上设置为syscall状态,然后对应的线程M进入系统调用阻塞等待。sysmom监控线程会定期扫描所有的P(处理器),若发现一个P处于syscall状态,就讲=将M(线程)和P(处理器)分离,并再分配一个M与这个P绑定,从而继续执行P本地队列中的其他goroutine。当之前阻塞的M从系统调用返回后,将其执行的goroutine放入全局G队列中,该M去sleep。