Goroutine调度
本文主要介绍Go的调度模型。
1. 线程实现模型
线程模型有三类:内核级线程模型、用户级线程模型、混合型线程模型。三者的区别主要在于线程与内核调度实体KSE(Kernel Scheduling Entity)之间的对应关系上。
内核调度实体KSE指操作系统内核调度器调度的对象实体,是内核调度的最小单元。
1.1. 线程模型对比
线程模型 | 用户线程与KSE之间的对应关系 | 特点 | 优点 | 缺点 |
---|---|---|---|---|
内核级线程模型 | 1:1 | 1条用户线程对应一条内核进程/线程来调度,即以核心态线程实现。 | 具有和内核线程一致的优点,不同用户线程之间不会互相影响。可以利用多核系统的优势。 | 在大量线程的情况下,线程的创建、删除、切换的代价更昂贵,影响性能。 |
用户级线程模型 | M:1 | N条用户线程只由一条内核进程/线程调度,即以用户态线程实现。 | 线程的创建、删除和环境切换都很高效。 | 一旦一个线程发生阻塞,整个进程下的其他线程也会被阻塞。不能利用多核系统的优势。 |
混合型线程模型 | M:N | M条用户线程由N条内核线程动态关联。又称两级线程模型 | 可以快速地执行上下文切换,而且可以利用多核的优势。当某个线程发生阻塞可以调度出CPU关联到可以执行的线程上。目前Go就是采用这种线程模型。 | 动态关联机制实现复杂,需要用户或runtime自己去实现。 |
1.2. 线程模型示意图
2. G-P-M调度模型
调度模型:
G-P-M对应关系:
2.1. 基本概念
- M:machine,代表系统内核进程,用来执行G。(工人)
- P:processor,代表调度执行的上下文(context),维护了一个本地的goroutine的队列。(小推车)
- G:goroutine,代表goroutine,即执行的goroutine的数据结构及栈等。(砖头)
2.2. 基本流程
调度的本质是将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是动态形式的一对多的关系。
2.3. 抢占式调度(阻塞)
当goroutine发生阻塞的时候,可以通过P将剩余的G切换给新的M来执行,而不会导致剩余的G无法执行,如果没有M则创建M来匹配P。
当阻塞的goroutine返回后,进程会尝试获取一个上下文(Context)来执行这个goroutine。一般是先从其他进程中"偷取"一个Context,如果"偷取"不成功,则将goroutine放入全局的goroutine中。
2.4. 偷任务
P可以偷任务即goroutine,当某个P的本地G执行完,且全局没有G需要执行的时候,P可以去偷别的P还没有执行完的一半的G来给M执行,提高了G的执行效率。
参考:
- http://morsmachine.dk/go-scheduler
- Scalable Go Scheduler Design Doc
- Go Preemptive Scheduler Design Doc
- k2huang/blogpost/Go并发机制
Feedback
Was this page helpful?
Glad to hear it! Please tell us how we can improve.
Sorry to hear that. Please tell us how we can improve.
最后修改 May 10, 2023: add code (7f583d9)