本文主要介绍Go的调度模型。
线程模型有三类:内核级线程模型、用户级线程模型、混合型线程模型。三者的区别主要在于线程与内核调度实体KSE(Kernel Scheduling Entity)之间的对应关系上。
内核调度实体KSE指操作系统内核调度器调度的对象实体,是内核调度的最小单元。
线程模型 | 用户线程与KSE之间的对应关系 | 特点 | 优点 | 缺点 |
---|---|---|---|---|
内核级线程模型 | 1:1 | 1条用户线程对应一条内核进程/线程来调度,即以核心态线程实现。 | 具有和内核线程一致的优点,不同用户线程之间不会互相影响。可以利用多核系统的优势。 | 在大量线程的情况下,线程的创建、删除、切换的代价更昂贵,影响性能。 |
用户级线程模型 | M:1 | N条用户线程只由一条内核进程/线程调度,即以用户态线程实现。 | 线程的创建、删除和环境切换都很高效。 | 一旦一个线程发生阻塞,整个进程下的其他线程也会被阻塞。不能利用多核系统的优势。 |
混合型线程模型 | M:N | M条用户线程由N条内核线程动态关联。又称两级线程模型 | 可以快速地执行上下文切换,而且可以利用多核的优势。当某个线程发生阻塞可以调度出CPU关联到可以执行的线程上。目前Go就是采用这种线程模型。 | 动态关联机制实现复杂,需要用户或runtime自己去实现。 |
调度模型:
G-P-M对应关系:
- M:machine,代表系统内核进程,用来执行G。(工人)
- P:processor,代表调度执行的上下文(context),维护了一个本地的goroutine的队列。(小推车)
- G:goroutine,代表goroutine,即执行的goroutine的数据结构及栈等。(砖头)
调度的本质是将G尽量均匀合理地安排给M来执行,其中P的作用就是来实现合理安排逻辑。
- P的数量通过
GOMAXPROCS()
来设置,一般等于CPU的核数,对于一次代码执行设置好一般不会变。 - P维护了一个本地的G队列(runqueue),包括正在执行和待执行的G,尽量保证所有的P都匹配一个M同时在执行G。
- 当P本地goroutine队列消费完,会从全局的goroutine队列(global runqueue)中拿goroutine到本地队列。P也会定期检查全局的goroutine队列,避免存在全局的goroutine没有被执行而"饿死"的现象。
- P和M是动态形式的一对一的关系,P和G是动态形式的一对多的关系。
当goroutine发生阻塞的时候,可以通过P将剩余的G切换给新的M来执行,而不会导致剩余的G无法执行,如果没有M则创建M来匹配P。
当阻塞的goroutine返回后,进程会尝试获取一个上下文(Context)来执行这个goroutine。一般是先从其他进程中"偷取"一个Context,如果"偷取"不成功,则将goroutine放入全局的goroutine中。
P可以偷任务即goroutine,当某个P的本地G执行完,且全局没有G需要执行的时候,P可以去偷别的P还没有执行完的一半的G来给M执行,提高了G的执行效率。
参考: