调度器的由来和分析
前言
我在看刘丹冰老师的视频教程Golang深入理解GPM模型时,发现《调度器的由来和分析》这章节有很多有同学在弹幕和评论区表示质疑和不解,所以我结合了一些操作系统的知识对这一章节进行的重构,创作了本文章。如果有出现纰漏,请及时指出。
用户态|内核态
我们的操作系统有两种状态,用户态和内核态,我们可以简单把用户态和内核态理解为平民阶级和特权阶级。应用程序在用户态下执行时,只能访问受限的资源和执行受限的指令集。内核态下运行的代码则可以直接操作硬件设备、更改系统资源状态、执行特权指令等。这是为了保护系统,因为一些特权指令是很危险的,例如清空内存。如果我们不得不使用用户态下的特权指令时,可以通过系统调用切换状态。值得注意的是系统调用发生在用户态,对系统调用的相关处理需要在内核态下进行。
关于更多用户态和内核态的知识,大家可以自行查询相关资料,但是对于接下来课题的学习,大家只要记住一点就好了:用户态和内核态的切换是一种昂贵的操作,涉及到上下文切换、权限切换等开销。
进程|线程|协程
进程、协程和线程是计算机中用于实现并发和并行的概念。它们表示了不同的执行单元和调度方式。
进程
- 进程是操作系统分配资源的最小单位,包含了程序的指令、数据和执行环境。
- 进程是一种重量级的执行单元,它的创建和销毁需要操作系统的介入,开销较大。
线程
- 线程是操作系统调度的最小执行单位,多个线程可以在同一个进程中并发执行。
- 线程是比进程更轻量级的执行单元,线程的创建和销毁的开销较小。
协程
- 协程是一种更高层级的并发实现方式,不同于线程,协程是由程序控制的,而不是由操作系统内核控制的。
- 协程可以在一个内核级线程内执行,通过协作式调度(非抢占)来实现多个协程之间的切换,避免了多线程之间的上下文切换开销,因此协程的切换开销非常小。
线程的实现
用户级线程 User-Lever Thread,ULT
早期的计算机系统(例如:Unix)是单核的,并不支持线程的,当时的线程是通过线程库来实现的,所有的线程管理工作包括线程切换都是应用程序负责,因为是应用程序控制,不用切换到内核态让CPU处理,所以用户线程管理的开销小,效率高),这种线程称为用户级线程。因为是应用程序负责管理,所以用户级线程的切换在用户态下就能实现,对于操作系统来说是无感知的。
协程也可以看成是一个轻量级的用户级线程
要注意的是用户级线程并不能真正地并行运行,是串行伪并行(可能是类似时间片轮询的算法,简单说就是当几个程序运行时,每个程序都运行很很小部分,然后在宏观上看是并行运行的。当然这里涉及到如何让程序中断后可以继续回到中断前的状态的问题,大家感兴趣可以自行查阅资料,这里就不展开了),这导致了一个问题,当一个线程阻塞时,整个进程都会阻塞,并发能力不强。
内核级线程 Kernel-Level Thread,KLT
随着时代发展,我们到达了多核处理器时代,这时候出现了可以由操作系统内核完全控制的线程,这种线程称为内核级线程。因为内核级线程是由操作系统直接控制的,不同的线程可以CPU不同的核心上处理,这就实现了真正的程序并发运行了。但是由此带来的代价是切换线程时不得不从用户态切换到核心态,结束后又从核心态切换回用户态,这带来了大量的系统切换开销。
用户级线程和内核级线程都有各自的优缺点,现代的多线程模型基本都是在内核级线程模型基础上,再次引入线程库,实现用户级线程,以达到更高的并发和更低的开销。但是这样就出现了一个问题:用户级线程和内核级线程该如何对接呢?这时候就出现了不同的多线程模型设计了。
多线程模型
一对一模型 1:1 One-to-One Model
我们把一个用户级线程对应一个内核级线程的模型称为一对一模型,这种模型的好处是当我们有一个用户级线程被阻塞了,其他用户级线程还能继续运行,每个用户级线程都可以在多核处理器上并行执行,并发能力强。但是缺点也很明显,和单独的内核级线程模型一样,线程管理不得不切换用户态/核心态,带来了大量的系统开销。
多对一模型 M:1 Many-to-One Model
我们把多个用户级线程共享一个内核级线程的多线程模型称为多对一模型。这种模型跟单独使用的用户级线程的模型类似,用户级线程的创建、调度和管理仍由用户程序或线程库完成,但所有的用户级线程都依附于一个内核级线程进行执行。这也意味着在多对一模型下,用户级线程无法在多核处理器上并行执行,因为它们都运行在同一个内核线程之上(当然如果你在一个进程下开多个内核级线程,多个内核级线程之间是可以并行的,这里指的一个进程下的一个内核级下的多个用户级线程,这些用户线程不能并行)。多对一模型的优点是创建和管理用户级线程的开销较低,适用于轻量级线程需求。
多对多模型 M:n Many-to-Many Model
多对多模型是指多个用户级线程映射到多个内核级线程。在多对多模型中,用户级线程的创建、调度和管理由用户程序或线程库完成,而内核级线程则由操作系统内核进行调度和管理。这种模型结合了一对一模型和多对一模型的优点,既可以实现较好的并发性,又可以控制线程的创建和管理开销。
Go的协程并发模型
我们把多对多模型中的用户级线程替换成goroutine(goroutine 其实就是go封装了一层的协程,本质上还是协程),把线程库替换成go的调度器,就可以得Go的协程并发模型了,其实本质是一样的。
通过上面层层递进学习,我相信大家已经懂了调度器的由来了,调度器在goroutine管理,平衡利用系统资源,实现高效的并发执行等方面的非常重要(相当于线程库至于用户线程的重要性)