在学习协程之前,我们再重新回顾一下进程,线程

  • 进程 (Process): 操作系统分配资源(内存、文件句柄等)的基本单位。每个进程有独立的地址空间。
  • 线程 (Thread): CPU 调度的基本单位。一个进程可以包含多个线程,这些线程共享进程的地址空间和资源。线程比进程更轻量级。

1. 内核线程 (Kernel Thread)

  • 定义: 内核线程是由操作系统内核直接创建、管理和调度的线程。也就是我们说的LWP(轻量级进程),在linux系统中,内核线程和用户态线程是1:1的。
  • 实现: 操作系统内核维护所有内核线程的上下文信息(如寄存器状态、栈指针等)。当需要进行线程切换时,内核会执行上下文切换。
  • 调度: 由操作系统的调度器负责调度。调度器根据优先级、时间片等策略决定哪个线程在哪个 CPU 上运行。
  • 开销:
    • 创建/销毁: 创建和销毁内核线程的开销相对较大,因为它涉及到系统调用和内核数据结构的操作。
    • 上下文切换: 上下文切换的开销也相对较大,因为它需要保存和恢复大量的寄存器信息,并且可能涉及到 TLB (Translation Lookaside Buffer) 刷新等操作。
  • 并行性: 多个内核线程可以在多核 CPU 上真正并行执行。
  • 阻塞: 当一个内核线程执行阻塞式系统调用(如 I/O 操作)时,只有该线程会被阻塞,同一进程中的其他内核线程可以继续运行。
  • 优点:
    • 能够充分利用多核 CPU 的并行能力。
    • 一个线程阻塞不会影响同一进程中的其他线程。
    • 由操作系统管理,更稳定可靠。
  • 缺点:
    • 创建、销毁和切换开销较大,不适合创建大量线程的场景。
    • 线程数量受限于操作系统资源。
  • 例子: Linux 上的 POSIX 线程 (pthreads) 在默认情况下就是内核线程(1:1 模型)。Windows 上的线程也是内核线程。

2. 用户态线程 (User-Level Thread)

  • 定义: 用户态线程是由用户空间的线程库(而不是操作系统内核)创建、管理和调度的线程。内核对这些线程一无所知,它只知道运行这些用户态线程的进程。
  • 实现: 线程库负责维护用户态线程的上下文信息(如程序计数器、栈指针、寄存器等)。线程的切换完全在用户空间完成,不涉及系统调用。
  • 调度: 由用户态的线程库自行调度。库可以实现自己的调度算法(例如,基于优先级、协作式调度或抢占式调度)。
  • 开销:
    • 创建/销毁: 开销非常小,因为不涉及系统调用。
    • 上下文切换: 开销非常小,因为完全在用户空间完成,不需要进入内核态。
  • 并行性: 多个用户态线程无法在多核 CPU 上真正并行执行。因为内核只知道一个进程,它会将整个进程调度到一个 CPU 上。即使进程中有多个用户态线程,在同一时刻,也只有一个用户态线程能运行。
  • 阻塞: 这是用户态线程的一个主要缺点。如果一个用户态线程执行了阻塞式系统调用,那么整个进程(包括所有其他用户态线程)都会被阻塞,直到该系统调用完成。这也是协程重点解决的问题,
  • 优点:
    • 创建、销毁和切换开销极小,可以创建非常大量的线程。
    • 用户可以自定义调度策略。
  • 缺点:
    • 无法利用多核 CPU 的并行性。
    • 一个用户态线程阻塞会导致整个进程阻塞。
    • 需要用户态线程库来管理,增加了编程复杂性。
  • 例子: 早期的 Java 线程(在某些 JVM 实现中)、一些绿色线程 (Green Thread) 实现。

3. 协程 (Coroutine)

  • 定义: 协程是一种用户态的、协作式多任务的程序组件。它不是由操作系统调度,而是由程序自身控制其执行流程。协程可以在执行过程中暂停,并将控制权交回给调用者(或另一个协程),然后在需要时从暂停的地方恢复执行。
  • 实现: 完全在用户空间实现,不涉及任何系统调用。协程的切换是非抢占式的,即一个协程只有在主动让出控制权时才会被切换。
  • 调度: 由程序逻辑或协程库负责调度。它不是操作系统调度器的一部分。
  • 开销:
    • 创建/销毁: 开销极小,通常只是分配一个小的栈空间。
    • 上下文切换: 开销极小,因为只保存和恢复最少的上下文信息(主要是程序计数器和栈指针),不涉及内核态切换,也没有 TLB 刷新等开销。
  • 并行性: 协程无法在多核 CPU 上并行执行。在任何给定时刻,一个进程中只能有一个协程在运行。它们是并发的,而不是并行的。
  • 阻塞: 协程是非阻塞的。当一个协程需要等待 I/O 或其他事件时,它会主动暂停执行,并将控制权交还给调度器。调度器可以运行其他协程,直到等待的事件完成,然后该协程再被恢复。这避免了整个进程或线程的阻塞。
  • 优点:
    • 极低的创建、销毁和切换开销,可以创建数百万个协程。
    • 避免了传统线程的同步问题(因为同一时间只有一个协程运行)。
    • 代码逻辑更清晰,可以用同步的方式编写异步代码(“回调地狱”的终结者)。
    • 非常适合 I/O 密集型任务。
  • 缺点:
    • 协作式调度: 如果一个协程内部有长时间的计算任务,它不主动让出控制权,就会阻塞整个进程,导致其他协程无法执行(“饿死”)。
    • 无法利用多核 CPU 的并行能力,如果需要并行计算,仍需结合多线程。
  • 例子: Python 的 asyncio,Go 语言的 Goroutines,Lua 的协程,C++20 的 Coroutines。

区别总结表

特性 内核线程 (Kernel Thread) 用户态线程 (User-Level Thread) LWP (Lightweight Process) 协程 (Coroutine)
管理方 操作系统内核 用户态线程库 操作系统内核 用户态程序/协程库
调度方 操作系统调度器 用户态线程库 操作系统调度器 程序逻辑/协程库
调度方式 抢占式 抢占式或协作式 抢占式 协作式
上下文切换 内核态,开销大 用户态,开销小 内核态,开销大 用户态,开销极小
并行性 可并行 (多核) 不可并行 可并行 (多核) 不可并行
阻塞影响 单个线程阻塞 整个进程阻塞 单个 LWP 阻塞 不阻塞 (主动让出控制)
创建数量 相对有限 可大量创建 相对有限 可大量创建 (百万级)
资源消耗 较大 较小 较大 极小
主要优点 利用多核,稳定 开销小,可定制调度 利用多核,提供执行上下文 开销极小,非阻塞 I/O,同步代码写异步逻辑
主要缺点 开销大 无法并行,阻塞整个进程 开销大 无法并行,协作式调度需小心
现代应用 Linux pthreads (1:1) 极少直接使用 Linux 内核线程的别称 Go Goroutines, Python asyncio, C++20 Coroutines

总结

  • 内核线程是操作系统提供的最基本的并发执行单元,开销相对大,但能充分利用多核。
  • 用户态线程是用户空间库实现的并发,开销极小,但无法并行,且阻塞时会影响整个进程。
  • LWP 是内核调度实体,在 Linux 中几乎等同于内核线程,它为用户态线程提供了内核上下文。
  • 协程是用户态的协作式任务,开销极小,通过主动让出控制权实现非阻塞,非常适合高并发 I/O 密集型任务,但无法并行。

这里简单总结一下,一个pthread对应一个内核线程,用户态线程:内核线程=n:1的,所以用户态线程无法并行执行,甚至遇到阻塞时连并发都做不到。

而对于协程而言,其实和用户态线程差不多,只是不会阻塞,也是只能做到并发而无法并行,比如python中的协程。但是go和java中的协程引入了调度器,实现m:n的映射,让多个协程映射到多个内核线程,从而可以实现真正的并行

但是这里引入了一个很有意思的问题,在同一个程序(进程)里面通过pthread库实现了多个内核线程,并将其绑定在不同的内核上,就要考虑缓存一致性这个问题。