GO调度器模型
GO的调度器可以充分利用多核心CPU,任何时候都有M个go协程在N个系统线程上进行调度, 这些线程在最多 GOMAXPROCS 个CPU核心上运行,这种调度模型称之为GMP模型:
- G : 协程(goroutine)
- M : 系统线程(machine)
- P : 处理器(processor)
如下图,每个P都有一个本地队列存放待运行的G,另外有一个全局队列,每个M需要依附在P上运行,一个P可以对应多个M, 但是同一时间一个P上只会有一个正在运行的M。
![b72b4f774f86f3bb4bf06309dd22a796.png](https://i-blog.csdnimg.cn/blog_migrate/68aa770ac0ce030a490c3469bf93915f.jpeg)
每轮调度只需要找一个可运行的G执行它就行,这个查找过程如下:
runtime.schedule() { // 1/61 的概率去全局队列找一个G来运行 // 如果没有找到,到本地队列中找 // 如果没有找到, // 尝试从其他的P中偷取G来运行 // 如果没有,检查全局队列 // 如果没有,检查 Net Poller}
GO协程的状态
GO协程和线程一样有三种状态,一个协程可以处于下面三种状态中的一种: Waiting , Runnable 和 Executing 。
- Waiting : 这种状态意味着协程被暂停运行需要等待某些事情完成才能继续运行。 比如等待一个系统调用的返回或者一个同步调用(原子或者锁操作)。这种类型的延时是性能低下的主要原因。
- Runnable : 这种状态说明协程正在等待被运行。如果有很多协程都在等待运行,那么协程需要等待一段更长的时间, 而且每个协程被分配的运行时间也会减少。这种调度延时也是一种性能低下的原因。
- Executing : 这种状态说明此协程被放在了M中正在执行它的指令。
调度时机
GO程序中的下列4类事件可以触发调度器执行调度任务。并不是说这些事件发生时调度器一定会执行调度,只是说此时调度器有机会执行调度:
- 使用 go 关键字的地方, go 关键字用于创建协程。在一个新的协程被创建的时候,就会给调度器一个机会执行调度任务。
- 垃圾回收,GC是在自己的一套协程中运行,所以在GC过程中需要被调度执行,在GC过程中调度器会优先调度需要接触堆内存的协程
- 系统调用,在系统调用时会导致协程阻塞这个M,调度器会将此协程调度出去或者使用一个新的M来执行队列中的其他协程。
- 同步和编排, atomic,mutex和channel操作都可能会阻塞这个协程,此时调度器可以调度一个新的协程来运行。
异步调用
大多数操作系统都支持网络轮询,例如MacOS的kqueue,Linux的epoll接口。Go会利用网络轮询接口来异步处理网络请求, 当G调用网络系统调用时调度器会将此G调度出去以避免M被阻塞,然后调度队列中的其他G继续执行,因此不需要创建一个新的M,减少调度开销。
在图1中,Goroutine-1正在M上运行,此时本地队列中有3个G在等待运行,网络轮询器上是空的。
![9d31523aec9128b4efe63bfb07501118.png](https://i-blog.csdnimg.cn/blog_migrate/a11b4f5df4e955b510e5af487940bcf2.jpeg)
图2中,Goroutine-1希望进行网络系统调用,此时将Goroutine-1移至网络轮询器上处理异步网络系统调用然后将Goroutine-2调度到M上继续运行。
![495f2ec70e3628e2a3370356898527de.png](https://i-blog.csdnimg.cn/blog_migrate/dc2025250059bf6ccf46abb8c7b41400.jpeg)
图3中,网络调用完成,此时Goroutine-1被放回本地队列中,当调度到Goroutine-1时,它可以继续执行接下来的指令,这里最大的好处是执行网络系统调用不需要额外的M, 网络轮询器实际是一个系统线程专门用于处理异步网络请求。
![f405fbe7a7331d27360580d328ddfa10.png](https://i-blog.csdnimg.cn/blog_migrate/570c1cb05d95a983ee7b602ac676ac54.jpeg)
同步调用
当G调用同步系统调用时会怎样?例如文件相关的系统调用以及使用CGO时调用C函数也是同步调用,此时M会被此G阻塞。
图4中,Goroutine-1正在M1上运行,他要执行同步系统调用,此时会阻塞M1。
![bc97244b0fde0eeee31e32177317d90f.png](https://i-blog.csdnimg.cn/blog_migrate/01fe445d0684b0917bc56a0a8769b332.jpeg)
图5中,调度器可以探测出M1被Goroutine-1阻塞了,此时调度器会将M1和P分开,但是Goroutine-1还是在M1上。 然后搞一个M2继续在P上运行,此时可以调度Goroutine-2继续执行。GO会维护一个线程池只有线程池中没有M才会创建新的M,所以这种M切换是非常快的。
![09355017894ef3d5322f26db33650b40.png](https://i-blog.csdnimg.cn/blog_migrate/122881d6907522166f837b1ae0f0f19e.jpeg)
图6中,Goroutine-1的同步阻塞调用完成,此时Goroutine-1会被转移到P的本地队列中待被调度执行,此时M1会被放在线程池待下次使用。
![9303653a7e1fd53cfec03e98036d068a.png](https://i-blog.csdnimg.cn/blog_migrate/f3b9317ffa0e243f80899df90627763b.jpeg)
任务窃取
任务窃取作用是平衡P之间的负载,如果某个P上的G都执行完了,此时会检查其他P上有没有可执行的G,如果有则会窃取其他P上的G来执行。
图7中,有两个P,每个P上有4个G,全局队列中也有一个G。
![a8fb6289097489253b6a3edb21c6ce12.png](https://i-blog.csdnimg.cn/blog_migrate/2f573fefe99b54967314ce2031463f68.jpeg)
图8中,P1上的G全部执行完了,但是P2和全局队列上还有G待执行。此时P1需要窃取其他G来执行,窃取规则和调度规则是一样的参考上面的 runtime.schedule 。
![af717ad3119901c3f3d4604d4ca05fdf.png](https://i-blog.csdnimg.cn/blog_migrate/c491443e99c164bace0396a4c1a75a6c.jpeg)
图9中,根据窃取规则,P1会将P2上一半的G窃取过来执行。
![7551e8c5c9f6ef5ff1212bd1efc7149f.png](https://i-blog.csdnimg.cn/blog_migrate/2624e3bd4fd5faaa7c51d63821302c6f.jpeg)
图10中,如果此时P2上的G都执行完了,并且P1的本地队列中也没有G了会怎么办?
![3282a2d4f0fea216c99f4dab063a496c.png](https://i-blog.csdnimg.cn/blog_migrate/3067f7bd20927c8f0e0419660f0fa503.jpeg)
图11中,P2上的G都执行完,它要开始窃取任务,但是P1上也没有G了,根据窃取规则他会把全局队列上的G拿过来执行。
![9dff0992e5d8dac5c864ee84642f6caf.png](https://i-blog.csdnimg.cn/blog_migrate/b41a194f1e41de2f977c5ba5fa849ef1.jpeg)
参考
这篇文章基本上是翻译下面的文章,然后加了一些自己的理解。