实现线程的两种方式 — 内核或用户进程
线程的实现:
- 由操作系统原生支持,用户进程通过系统调用使用线程。线程在 0 特权级的内核空间中实现(并不是线程所运行的代码也必须是 0 特权级的内核级代码,也可以是 3 特权级的用户级代码)
- 进程自己实现线程,线程在 3 特权级的用户空间实现。通常情况下,标准库提供了用户级线程库,我们直接调用即可
线程仅仅是个执行流,在用户空间,还是在内核空间实现它,最大的区别就是线程表在哪里,由谁来调度它上处理器。
- 如果线程在用户空间中实现,线程表就在用户进程中,用户进程就要专门写个线程用作线程调度器,由他来调度进程内部的其他线程
- 如果线程在内核空间中实现,线程表就在内核中,该线程就会由操作系统的调度器统一调度,无论该线程属于内核,还是用户进程
一、在用户空间实现线程?
- 在用户空间中实现线程的好处就是可移植性强,由于是用户级的实现,所以在不支持线程的操作系统上也可以写出完美支持线程的用户程序
- 在用户空间实现线程,操作系统不会意识到线程的存在,因为操作系统调度器只会以整个进程的方式调度,将处理器的使用权交给这个进程,由进程中的调度器自己去协调分配处理器时间。
- 线程调度涉及到调度器以及线程表。因此,进程还要自己在进程内维护线程表
- 这种方式一般由权威机构发布线程库,开发人员使用此库即可。
在用户空间实现线程的优缺点:
优点:线程调度算法由用户程序自己实现,可以根据实际情况为某些线程加权调度
优点:将线程的寄存器映像装载到 CPU 时,可以在用户空间完成,即不用陷入内核态,这样就免去了进入内核时的入栈和出栈操作
缺点:进程中某个线程如果出现了阻塞(通常由于系统调用引起),操作系统不知道进程中存在线程,操作系统会认为他是传统型进程(单线程进程),因此会将整个进程挂起,导致进程中所有线程都无法运行。
缺点:在用户空间实现线程,对于操作系统来说,调度器的调度单元是整个进程,并不是进程中的线程,所以时钟中断只能影响进程一级的执行流。
进程中某个线程在处理器上运行后,只要该线程不主动让出 CPU,此进程中的其他线程都没有机会运行。也就是说,可能会出现单一线程过度使用 CPU,其他线程没有调度的机会。只能凭借线程主动让出 CPU(通过类似 pthread_yield、pthread_exit),给进程中的其他线程。
缺点:线程在用户空间实现线程,和在内核空间实现相比,只是在内部调度时少了陷入内核的代价,但由于整个进程占据 CPU 的时间片是有限的,这有限的时间片还要再分给内部的线程,所以每个线程执行的时间片非常短暂,再加上进程内线程调度器维护线程表、运行调度算法的时间片消耗,反而抵消了内部调度带来的提速
二、在内核空间实现线程
线程管理的所有工作(创建和撤销)由操作系统内核完成
在内核空间中实现线程的优缺点:
- 优点:内核提供的线程相当于让进程多占了 CPU 资源。比如进程 A 创建了 3 个线程,那么在系统中就一共有 4 个线程在和其他进程或者线程在竞争 CPU 时间。
- 优点:当进程中某一线程阻塞后,操作系统只会阻塞这个线程,而这个进程中的其他线程不受影响。就有相当于提速了
- 缺点:用户进程需要通过系统调用陷入内核,这里会增加一些现场保护的栈操作,这还是会消耗一些处理器时间。但和上面两个提速相比,这点消耗可以接受。
三、线程实现的组合策略
由操作系统内核支持内核级多线程,由操作系统的程序库来支持用户级多线程,线程创建完全在用户空间创建,线程的调度也在应用程序内部进行,然后把用户级多线程映射到(或者说是绑定到)一些内核级多线程。编程人员可以针对不同的应用特点调节内核级线程的数目来达到物理并行性和逻辑并行性的最佳方案。
1. 多对一(Many to One)
多个用户线程对应同一个内核线程。线程的创建、调度、同步的所有细节全部由进程的用户空间线程库来处理。这样极大的减少了创建内核态线程的成本,但是线程不可以并行。因此这种模型用的很少。
用户态线程如何使用内核态线程执行程序?程序是存储在内存中的指令,用户态线程是可以准备好程序让内核态线程执行的
- 优点:用户线程的很多操作对内核来说都是透明的,不需要用户态和内核态的频繁切换。这样线程的创建、调度、同步会很快
- 缺点:由于多个用户线程对应到同一个内核线程,如果其中一个用户线程阻塞,那么其他用户线程也无法执行
- 缺点:内核并不知道用户态有哪些线程,无法像内核线程一样实现较完整的调度、优先级等
2. 一对一(One to One)
每个用户态线程都通过系统调用创建一个绑定的内核线程,并附加在上面执行。这种模型允许所有线程并行执行,能够充分利用多核优势。目前 Linux 中的线程、OpenJDK Java 线程等采用的都是一对一线程模型。每一个JVM线程,都有一个对应的内核线程。
- 缺点:每创建一个用户线程,相应地就需要创建一个内核线程,开销较大,因此需要限制整个系统的线程数量。
- 缺点:用户线程的大部分操作都会映射到内核线程上,引起用户态和内核态的频繁切换。
3. 多对多(Many to Many)
这种模式下会为 n 个用户态线程分配 m 个内核态线程,m 通过小于 n。一种可行的策略是将 m 设置为 CPU 核心数。这种多对多的关系,减少了内核线程,同时也保证了多核心并行。多对多模型中线程的调度需要由内核态和用户态一起来实现。例如线程间同步需要用户态和内核态共同实现。用户态和内核态的分工合作导致实现该模型非常复杂。
Linux 采用一对一的模型,我们的系统采用在内核空间中实现线程。