多线程模型
多线程模型概述
在现代操作系统中,为了支持并发执行,有两种主要的线程类型:用户级线程(User-Level Threads, ULTs) 和 内核级线程(Kernel-Level Threads, KLTs)。而多线程模型则描述了用户级线程与内核级线程之间的映射关系。换句话说,它定义了用户编写的线程(用户级线程)是如何被操作系统内核所管理的线程(内核级线程)来支持的。
理解这两种线程是理解多线程模型的基础。
-
用户级线程 (ULTs):
- 特点:完全由用户空间的线程库管理。内核对它们的存在无感知。线程的创建、销毁、调度、同步都在用户空间完成,不需要内核介入。
- 优点:
- 创建与管理开销小:因为不需要内核模式切换,所以创建和销毁线程的速度快,上下文切换效率高。
- 调度灵活:用户线程库可以根据应用需求实现自定义的调度算法。
- 缺点:
- 阻塞性:正如我们之前讨论的,如果一个用户级线程执行阻塞式系统调用,整个进程(包括进程内的所有用户级线程)都会被内核阻塞,导致其他用户级线程也无法运行。
- 无法利用多核CPU:由于内核只将整个进程视为一个执行单元,因此在一个多核处理器上,一个进程内部的用户级线程无法同时在不同的CPU核心上并行执行。
- 应用场景:适合于I/O操作较少,计算密集型且需要频繁切换的场景。
-
内核级线程 (KLTs):
- 特点:由操作系统内核管理和调度。每个内核级线程都是内核可独立调度的实体。
- 优点:
- 非阻塞性:当一个内核级线程执行阻塞式系统调用时,只有这个线程会被阻塞,同一进程内的其他内核级线程可以继续执行,提高了并发性。
- 可利用多核CPU:内核可以同时调度多个内核级线程在不同的CPU核心上并行执行,真正实现并行计算。
- 缺点:
- 创建与管理开销大:线程的创建、销毁、调度等操作都需要陷入内核,涉及用户态/内核态的切换,开销相对较大。
- 可移植性差:线程的实现细节与特定的操作系统内核紧密相关。
- 应用场景:适用于I/O密集型任务或需要充分利用多核CPU的场景。
常见的多线程模型
基于用户级线程和内核级线程的不同映射关系,主要有三种多线程模型:
1. 多对一模型 (Many-to-One Model)
- 特点:
- 将多个用户级线程映射到一个内核级线程上。
- 所有用户级线程的调度和管理都由用户空间的线程库完成。
- 对于内核来说,只存在一个内核级线程,因此内核只知道一个进程在运行。
- 优点:
- 用户级线程的管理开销小,创建和切换速度快。
- 缺点:
- 最大的缺点就是阻塞问题:如果任何一个用户级线程执行阻塞系统调用,整个进程及其所有用户级线程都会被阻塞。
- 无法利用多核处理器的并行能力。
- 典型实现:较早期的线程库,如Solaris Green Threads。在现代操作系统中已经不常见,或仅用于特定目的(如Go语言的早期协程)。
- 常考点:强调其“一个阻塞则全部阻塞”的特性,以及无法利用多核的缺点。
2. 一对一模型 (One-to-One Model)
- 特点:
- 将每一个用户级线程都独立地映射到一个内核级线程上。
- 内核可以感知并调度每一个线程。
- 优点:
- 真正的并发性:一个线程阻塞不会影响其他线程的执行。
- 可以充分利用多核处理器,实现真正的并行。
- 缺点:
- 开销大:线程的创建、销毁、上下文切换都需要内核介入,涉及系统调用,开销比用户级线程大。
- 操作系统对可以创建的内核级线程数量通常有限制,创建过多可能导致性能下降。
- 典型实现:Linux的Pthreads(NPTL实现),Windows线程。这是现代大多数操作系统所采用的主流模型。
- 常考点:强调其高并发性、能利用多核的优点,以及系统开销大的缺点。
3. 多对多模型 (Many-to-Many Model)
- 特点:
- 将多个用户级线程映射到数量相等或更少(但大于一)的内核级线程上。
- 它试图结合前两种模型的优点,避免它们的缺点。
- 用户线程库负责用户级线程的调度,而内核负责内核级线程的调度。
- 优点:
- 灵活性:用户级线程的创建和管理效率高。
- 解决了多对一模型中一个线程阻塞导致整个进程阻塞的问题(因为有多个内核级线程可以接管)。
- 在一定程度上可以利用多核处理器。
- 可以为应用程序创建大量用户级线程,而只需要较少的内核级线程来支持。
- 缺点:
- 实现复杂:需要用户线程库和内核调度器之间的复杂协调。
- 性能调优困难。
- 典型实现:Solaris的早期版本、Windows NT早期版本,现在较少有纯粹的实现。Go语言的Goroutines(协程)在底层也常被实现为类似多对多模型的调度方式。
- 常考点:通常作为一种理想模型出现,但其实现复杂性也是考点。