c++中的协程(一)
在学习协程之前,我们再重新回顾一下进程,线程
- 进程 (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库实现了多个内核线程,并将其绑定在不同的内核上,就要考虑缓存一致性这个问题。
All articles on this blog are licensed under CC BY-NC-SA 4.0 unless otherwise stated.
Comments