第10讲 进程与线程II

进程与线程 II:线程模型、TCB/TLS 与纤程协程的完整理解

本文根据《第10讲 进程与线程II.md》的对话内容整理而成,目标是把原始问答重构为一篇可独立阅读、可复习、可维护的正式学习文档。


引言:本讲的核心问题

“进程与线程 I”主要解释进程:程序如何变成进程,操作系统如何用 PCB 管理进程,以及 forkexecwaitexit 如何构成进程生命周期。

“进程与线程 II”的主线则进一步推进:

进程太重,所以引入线程;内核线程仍然有开销,所以进一步引入用户态线程、纤程和协程。

本讲要解决的核心问题不是“线程是什么”这一句定义,而是以下几个问题:

  1. 为什么进程内部还需要线程?
  2. 线程和进程到底共享什么、独占什么?
  3. 用户态线程和内核态线程如何区分?
  4. 多对一、一对一、多对多模型中的“对”到底是什么意思?
  5. TCB 和 TLS 分别解决什么问题?
  6. pthread 的创建、等待、退出和让出 CPU 如何理解?
  7. 线程上下文切换到底发生了什么?
  8. 纤程和协程为什么比线程更轻量?

整节课的逻辑可以概括为:

1
2
3
4
5
6
7
进程太重

线程:进程内更轻量的执行流

内核线程仍有创建与切换开销

用户态线程 / 纤程 / 协程:由用户态或语言运行时调度的轻量执行流

1. 为什么需要线程

进程是操作系统管理程序运行的基本抽象,但进程本身比较重。一个进程不仅包含执行状态,还包含代码、数据、堆、栈、页表、文件描述符、地址空间等资源。创建一个进程,意味着操作系统要准备一整套运行环境。

这带来三个问题。

第一,创建进程开销大。
如果一个任务很短,却每次都创建一个新进程,创建和销毁本身就会变成明显负担。

第二,进程间通信开销大。
不同进程拥有不同地址空间,彼此不能直接访问对方的数据。如果要交互,通常需要 IPC。IPC 安全,但比同一地址空间内的变量访问、函数调用要重得多。

第三,单个进程内部需要多个执行流。
例如浏览器可能同时需要处理页面渲染、网络请求、音频播放、用户输入和脚本执行。如果一个进程内部只有一个执行流,这些任务就只能排队执行。

因此,操作系统引入线程:

线程是进程内部更轻量的执行流。

线程让一个进程内部可以同时存在多个执行路径。它们共享进程提供的资源,但各自拥有自己的运行状态。


2. 线程和进程的基本区别

可以用一句话区分进程和线程:

进程是资源分配单位,线程是 CPU 调度和执行单位。

进程像一家公司,拥有办公楼、资产、文件和制度;线程像公司里的员工,每个员工有自己的工作状态和执行路线,但共享公司的资源。

对比项 进程 线程
基本角色 资源分配单位 执行与调度单位
地址空间 进程之间通常独立 同一进程内线程共享
创建开销 较大 较小
通信方式 IPC,较重 共享内存,较轻
隔离性
崩溃影响 通常影响本进程 一个线程出错可能带崩整个进程
调度 早期可作为调度单位 现代系统主要调度线程

线程自己的内容主要包括:

  • 程序计数器 PC;
  • 寄存器状态;
  • 用户栈;
  • 内核栈;
  • 线程状态;
  • 线程优先级;
  • 调度相关信息。

线程共享的内容主要包括:

  • 代码段;
  • 数据段;
  • 堆;
  • mmap 区域;
  • 文件描述符;
  • 进程级资源;
  • 同一个虚拟地址空间。

因此,多线程的本质是:

多个执行流共享同一套进程资源,但每个执行流有自己的运行现场。


3. “共享地址空间”不等于“所有变量都共享”

一个容易误解的点是:既然同一进程内多个线程共享地址空间,那是不是所有变量都共享?

答案是否定的。

更准确地说:

多线程共享同一个虚拟地址空间,但并不意味着所有变量实例都只有一份。

多线程中的变量可以分成三类。

3.1 普通全局变量:通常共享

例如:

1
int global_count = 0;

多个线程访问的是同一个 global_count。如果多个线程同时执行:

1
global_count++;

就可能发生数据竞争。因为 ++ 通常不是一个原子操作,而可能被拆成:

  1. 读取变量;
  2. 加一;
  3. 写回变量。

多个线程交错执行时,可能造成更新丢失。


3.2 栈变量:每个线程天然独有

例如:

1
2
3
4
void *func(void *arg) {
int x = 0;
x++;
}

如果多个线程都执行 func,每个线程都有自己的用户栈,所以每个线程都有自己的 x。这些变量名字一样,但位于不同线程的栈上,不是同一个内存位置。


3.3 TLS 变量:形式像全局变量,但每个线程有一份

例如:

1
__thread int id;

这类变量看起来像全局变量,但每个线程都有自己的副本。

可以理解为:

1
2
3
线程 1 的 TLS 区:id = 1
线程 2 的 TLS 区:id = 2
线程 3 的 TLS 区:id = 3

代码里都写 id,但不同线程访问的是自己 TLS 区中的 id

所以更精确的结论是:

同一进程内的多线程共享地址空间;普通全局变量和堆对象通常共享;每个线程的栈变量天然独立;被声明为 TLS 的变量虽然形式上像全局变量,但每个线程有自己的副本。


4. 用户态线程和内核态线程

线程可以按照“是否由内核直接管理”分成两类:

  1. 内核态线程;
  2. 用户态线程。

这里的“内核态线程”并不是说这个线程永远运行在内核态;“用户态线程”也不是说它只能执行普通用户代码。真正的区分点是:

谁知道它,谁管理它,谁调度它。


4.1 内核态线程

内核态线程是由操作系统内核创建、记录、调度和切换的线程。

它的特点是:

  • 内核知道它的存在;
  • 内核调度器可以直接调度它;
  • 它通常有对应的内核数据结构;
  • 它可以被分配到不同 CPU 核心上运行;
  • 创建和切换通常涉及内核,开销相对更大。

例如,在主流 Linux 实现中,pthread_create 创建的 pthread 通常对应一个内核调度实体。这个线程执行用户函数时当然是在用户态运行,但它背后的调度对象由内核管理。

因此,“内核态线程”的重点不是它运行的代码位置,而是它是否是内核可见、内核可调度的对象。


4.2 用户态线程

用户态线程是由用户态线程库或语言运行时管理的轻量执行流。内核通常不知道每一个用户态线程的存在。

它的特点是:

  • 内核不直接知道它;
  • 用户态运行时或线程库管理它;
  • 用户态调度器负责切换它;
  • 切换通常不需要进入内核;
  • 创建和切换开销较小;
  • 是否能利用多核,取决于它映射到底层几个内核线程上。

可以用下面的结构理解:

1
2
3
4
5
内核眼中:
一个内核线程 K

用户态运行时眼中:
K 上轮流运行 u1、u2、u3、u4

内核只看见 K,但用户态运行时知道自己内部还有多个用户态执行流。


4.3 二者对比

对比项 用户态线程 内核态线程
内核是否直接可见 通常不可见 可见
谁创建 用户态线程库 / 语言运行时 操作系统内核
谁调度 用户态调度器 内核调度器
切换是否进入内核 通常不需要 通常需要
切换开销 较大
多核并行能力 取决于映射到几个内核线程 可以直接被内核分配到多核
阻塞影响 一个底层内核线程阻塞可能影响其上多个用户线程 一个线程阻塞通常不影响其他内核线程
典型例子 ucontext、fiber、协程运行时内部任务 pthread 背后的 Linux task

核心结论是:

用户态线程和内核态线程的区别,不在于代码是否运行在用户态,而在于线程由谁管理、谁调度、内核是否看得见。


5. 三种线程模型中的“对”是什么意思

线程模型中的“多对一”“一对一”“多对多”,讨论的是:

用户态线程和内核态线程之间的映射关系。

这里的“对”可以理解为:

上层用户态执行流,如何借助下层内核调度实体获得 CPU 执行机会。

用户态线程本身不能直接占用 CPU。真正能被操作系统调度到 CPU 上执行的是内核态线程。因此,用户态线程必须映射或挂靠到某个内核线程上,才能真正运行。

可以把用户态线程理解为“任务”,把内核态线程理解为“跑道”:

模型 类比
多对一 很多任务共用一条跑道
一对一 一个任务配一条跑道
多对多 很多任务共用多条跑道

6. 多对一模型

多对一模型是:

多个用户态线程映射到一个内核态线程。

1
2
3
用户态: u1   u2   u3   u4
\ | | /
内核态: K1

在这个模型中,内核只知道一个内核线程 K1,不知道用户态内部还有 u1u2u3u4

它的优点是:

  • 用户态线程创建快;
  • 用户态切换快;
  • 不需要频繁进入内核;
  • 内核管理简单。

它的缺点也很明显:

  • 不能真正利用多核;
  • 一个底层内核线程阻塞,可能影响其上的所有用户态线程;
  • 可扩展性较差。

最直白地说:

多对一模型中,用户态觉得“我有很多线程”,但内核觉得“你只有一个线程”。


7. 一对一模型

一对一模型是:

一个用户态线程对应一个内核态线程。

1
2
3
用户态: u1   u2   u3
| | |
内核态: K1 K2 K3

在这个模型中,每个用户态线程都有一个对应的内核线程。内核能看见每个线程,也能分别调度它们。

它的优点是:

  • 可以利用多核;
  • 一个线程阻塞,不一定阻塞其他线程;
  • 调度由内核统一管理;
  • 更符合现代操作系统的调度机制。

它的缺点是:

  • 内核线程数量可能很多;
  • 创建、销毁、切换都有内核开销;
  • 大量短命线程会造成明显负担。

Linux、Windows、macOS 等主流系统通常采用一对一模型。

最直白地说:

一对一模型中,用户态觉得“我有几个线程”,内核也基本觉得“你有几个线程”。


8. 多对多模型

多对多模型是:

N 个用户态线程映射到 M 个内核态线程,通常 N > M。

1
2
3
用户态: u1   u2   u3   u4   u5   u6
\ | / \ | /
内核态: K1 K2

它试图同时解决多对一和一对一的问题。

相比多对一,它可以利用多个内核线程,从而更好地利用多核。
相比一对一,它不要求每个用户态线程都对应一个内核线程,可以减少内核线程数量。

它的优点是:

  • 能利用多核;
  • 能控制内核线程数量;
  • 适合大量用户态执行流;
  • 理论上更加灵活。

它的缺点是:

  • 管理复杂;
  • 用户态调度器和内核调度器需要配合;
  • 实现难度高。

最直白地说:

多对多模型中,用户态线程很多,但真正交给内核调度的线程数量被控制住。


9. TCB:线程控制块

之前学习进程时,有 PCB,即 Process Control Block。对应地,线程也有 TCB,即 Thread Control Block。

TCB 用于保存线程相关信息,例如:

  • 线程 ID;
  • 线程状态;
  • 线程优先级;
  • 寄存器上下文;
  • 栈指针;
  • TLS 信息;
  • 调度相关信息。

如果说 PCB 是进程的“档案袋”,那么 TCB 就是线程的“档案袋”。

在线程模型中,TCB 可能分成内核态部分和用户态部分:

部分 作用
内核态 TCB 由内核维护,支持调度、上下文切换、内核栈等
用户态 TCB 由线程库维护,保存 pthread 结构、用户栈、TLS 等

在 Linux 中,进程和线程在内核层面都可以看作 task,常用同一种核心数据结构表示。它们的差别主要不在于“是不是 task”,而在于共享资源的程度不同。

可以概括为:

Linux 内核中,进程和线程都可被看作调度实体;线程与进程的差异主要体现在地址空间、文件表等资源是否共享。


10. TLS:线程本地存储

TLS 是 Thread Local Storage,即线程本地存储。它解决的问题是:

多线程共享地址空间,但有些变量需要每个线程各自拥有一份。

普通全局变量默认共享:

1
int global_id;

如果线程 1 修改它,线程 2 也能看到。

但有些变量虽然写起来像全局变量,却不应该被所有线程共享。例如 errno。如果多个线程同时调用系统库函数出错,每个线程都应该看到自己的错误码,而不是互相覆盖。

因此需要 TLS:

1
__thread int id;

它表示每个线程都有自己的 id

TLS 的访问通常可以通过 TCB 或特定线程寄存器完成,寻址方式可以理解为:

1
线程本地区域基地址 + 变量偏移量

所以,TLS 的关键不是“变量名不同”,而是:

同一个变量名,在不同线程中对应不同的存储实例。


11. pthread 的基本操作

pthread 是 POSIX 线程库,常见操作包括:

  1. pthread_create
  2. pthread_join
  3. pthread_exit
  4. pthread_yield

11.1 pthread_create:创建线程

典型形式如下:

1
pthread_create(&thread, NULL, function, arg);

它的作用是创建一个新线程,并让新线程从指定函数开始执行。

在一对一模型下,pthread_create 往往涉及两层工作:

层次 工作
内核态 创建对应内核线程,准备内核栈,加入调度系统
应用态 创建 pthread 结构,准备用户栈,建立 TLS

因此,pthread 虽然比进程轻,但并不是没有成本。


11.2 pthread_join:等待线程结束

典型形式如下:

1
pthread_join(thread, NULL);

它的作用是:

  • 等待指定线程结束;
  • 获取该线程的返回值;
  • 回收相关线程资源。

可以把它理解为:

pthread_create 分出一条执行流,pthread_join 等这条执行流结束。

因此,join 有点像 fork 的逆向操作:fork/create 是分出去,join 是等回来。


11.3 pthread_exit:线程主动退出

典型形式如下:

1
pthread_exit(retval);

它让当前线程退出,并可以提供返回值。这个返回值可以被其他线程通过 pthread_join 获取。


11.4 pthread_yield:主动让出 CPU

pthread_yield 的含义是:

当前线程主动暂停,让调度器有机会运行其他线程。

它不是结束线程,而是当前线程说:“我现在可以先不运行,CPU 可以给其他线程。”


12. join 放在哪里:串行还是并行

线程题里,一个高频考点是 join 的位置。

如果写成:

1
2
3
4
5
6
7
8
pthread_create(&p1, NULL, mythread, NULL);
pthread_join(p1, NULL);

pthread_create(&p2, NULL, mythread, NULL);
pthread_join(p2, NULL);

pthread_create(&p3, NULL, mythread, NULL);
pthread_join(p3, NULL);

这基本不是并行执行。因为每创建一个线程,主线程马上等待它结束。执行顺序近似为:

1
2
3
p1 完成
p2 完成
p3 完成

如果每个线程执行:

1
2
3
for (int i = 0; i < 200; i++) {
balance++;
}

那么结果通常稳定为:

1
2
3
4
p1 后 balance = 200
p2 后 balance = 400
p3 后 balance = 600
Final Balance = 600

但如果写成:

1
2
3
4
5
6
7
pthread_create(&p1, NULL, mythread, NULL);
pthread_create(&p2, NULL, mythread, NULL);
pthread_create(&p3, NULL, mythread, NULL);

pthread_join(p1, NULL);
pthread_join(p2, NULL);
pthread_join(p3, NULL);

这时三个线程可能并行执行。由于 balance++ 不是原子操作,最终结果不一定可靠。循环次数越大,数据竞争越容易表现出来。

核心判断方法是:

创建一个就立刻 join:基本串行。
先全部 create,再全部 join:可能并行,需要考虑数据竞争。


13. 线程上下文切换

线程上下文切换是本讲较硬的部分。它回答的是:

CPU 如何从线程 A 切换到线程 B,并让 B 从上次暂停的位置继续执行?

线程上下文通常包括:

  • 通用寄存器;
  • 程序计数器 PC;
  • 用户栈指针;
  • 内核栈指针;
  • CPU 状态寄存器;
  • 页表相关寄存器;
  • 其他体系结构相关状态。

以一对一线程模型为例,线程是内核调度对象,因此线程切换通常需要进入内核。

整体流程可以分为三步:

  1. 进入内核态并保存当前线程上下文;
  2. 切换页表和内核栈;
  3. 恢复目标线程上下文并返回用户态。

13.1 第一步:进入内核态并保存上下文

线程 A 正在用户态执行时,可能因为以下原因进入内核:

  • 时钟中断;
  • 系统调用;
  • 异常;
  • 缺页;
  • I/O 事件。

进入内核后,硬件通常会自动保存一部分状态,例如:

  • 用户态 PC;
  • CPU 状态;
  • 用户栈与内核栈切换相关信息。

然后,内核软件继续保存通用寄存器等状态到线程 A 的内核栈或 TCB 中。


13.2 第二步:切换页表和内核栈

调度器选择线程 B 后,内核需要切换到 B 的运行环境。

这一步包括:

  • 切换地址空间或页表;
  • 找到线程 B 的 TCB;
  • 找到线程 B 的内核栈;
  • 修改内核栈指针,切换到 B 的内核栈。

这里有一个非常重要的理解:

切换内核栈可以看作线程切换的分界点。

原因是:切换内核栈之后,接下来恢复上下文时,恢复代码会从目标线程 B 的内核栈中取出 B 之前保存的上下文。也就是说,从此刻开始,内核虽然还在执行同一段切换代码,但它已经站在 B 的执行现场上继续恢复了。


13.3 第三步:恢复上下文并返回用户态

内核从线程 B 的保存区域中恢复:

  • 通用寄存器;
  • 用户栈指针;
  • 程序计数器;
  • CPU 状态;
  • 其他必要状态。

最后执行返回用户态的指令。此时 CPU 回到线程 B 的用户态执行位置,B 像是从上次被暂停的地方继续运行。

可以用一条链路表示:

1
2
3
4
5
6
7
线程 A 用户栈

线程 A 内核栈

线程 B 内核栈

线程 B 用户栈

这说明线程上下文切换不仅是“保存和恢复寄存器”,还涉及权限级别、栈、页表、调度实体等多个层面的切换。


14. 一对一线程模型的局限

一对一线程模型是主流系统常用模型,但它并不完美。

它的主要问题有两个。

第一,内核调度器不了解应用内部逻辑。
例如生产者-消费者模型中,生产者刚生产完数据后,消费者最好马上运行。但内核调度器未必知道这种应用级关系,因此不一定做出最符合业务逻辑的调度选择。

第二,短命线程开销太大。
如果一个 Web 请求只需要亚毫秒级时间,但每个请求都创建一个内核线程,那么创建、销毁和上下文切换的成本可能超过任务本身。

因此,需要更轻量、更接近用户态逻辑的执行流,这就引出了纤程和协程。


15. 纤程:用户态管理的轻量执行流

纤程可以理解为:

用户态管理的轻量级线程。

它不单独对应一个内核线程。一个内核线程上可以运行多个纤程。

内核眼中可能只有:

1
一个内核线程 K

但用户态运行时眼中有:

1
纤程 f1、f2、f3、f4

纤程的优点包括:

  • 不需要创建新的内核线程;
  • 创建开销小;
  • 切换不需要进入内核;
  • 上下文切换快;
  • 用户态可以自主调度;
  • 更适合短任务和高并发场景。

但它也有限制:

  • 内核不能直接调度每个纤程;
  • 如果底层内核线程阻塞,其上的纤程可能受到影响;
  • 是否能利用多核,取决于运行时如何映射到底层内核线程。

可以对比如下:

对象 谁管理 切换是否进内核 能否被内核直接调度
内核线程 操作系统 通常需要 可以
纤程 用户态库 / 运行时 通常不需要 不可以
协程 编程语言运行时 通常不需要 不可以直接调度

16. ucontext:用户态上下文切换

Linux 中曾提供 ucontext 相关接口,用于保存和切换用户态上下文。常见接口包括:

接口 作用
getcontext 保存当前上下文
setcontext 切换到指定上下文
makecontext 创建新的上下文

每个 ucontext 可以看成一个用户态执行流的上下文。

经典例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int x = 0;
ucontext_t context, *cp = &context;

void func(void) {
x++;
setcontext(cp);
}

int main(void) {
getcontext(cp);
if (!x) {
printf("getcontext has been called\n");
func();
} else {
printf("setcontext has been called\n");
}
}

输出是:

1
2
getcontext has been called
setcontext has been called

原因如下:

  1. 第一次执行 getcontext(cp),保存当前上下文。
  2. 此时 x = 0,进入 if,打印第一句。
  3. 调用 func(),执行 x++,使 x = 1
  4. setcontext(cp) 回到之前保存的上下文。
  5. 程序像是再次从 getcontext(cp) 返回。
  6. 但此时 x = 1,所以进入 else,打印第二句。

这个例子说明:

用户态上下文切换可以让程序跳回一个先前保存的执行现场,但全局状态的变化仍然保留。


17. 协程:语言级轻量执行流

协程和纤程非常接近,但使用语境略有不同:

  • 纤程偏操作系统或运行时概念;
  • 协程偏编程语言概念。

协程通常由语言运行时管理,常见于 Go、Python、Lua、C++20 等语言或标准中。

协程常见状态包括:

  • 新生;
  • 运行;
  • 暂停;
  • 终止。

协程的核心操作是:

操作 含义
yield 主动暂停,把控制权交出去
resume 恢复执行,从暂停位置继续

协程的优势在于:

  • 创建轻量;
  • 切换轻量;
  • 适合大量并发任务;
  • 可以由语言运行时根据应用逻辑调度;
  • 不必每个任务都对应一个内核线程。

18. Go 协程:goroutine

Go 语言使用 go 关键字启动 goroutine:

1
go asyncTask()

这表示启动一个轻量执行流,由 Go 运行时管理。

典型网络服务器例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
func handler(c net.Conn) {
c.Write([]byte("ok"))
c.Close()
}

func main() {
l, _ := net.Listen("tcp", ":8000")
for {
c, _ := l.Accept()
go handler(c)
}
}

每来一个连接,就启动一个 goroutine 处理。

如果使用传统内核线程,每个请求一个线程,开销可能很大。goroutine 则可以由 Go 运行时把大量轻量任务映射到较少的内核线程上,适合高并发网络服务。

因此,goroutine 可以看作一种语言运行时管理的轻量执行流。它不是简单等同于内核线程,而是由 Go runtime 调度和管理。


19. 核心概念总表

概念 核心含义 谁管理 是否共享地址空间 开销
进程 资源分配单位 内核 进程间通常不共享 最大
线程 进程内执行流 内核 / 线程库 同进程线程共享 中等
用户态线程 用户态管理的执行流 用户态库 / 运行时 通常共享
内核态线程 内核调度实体 内核 取决于所属进程 中等
纤程 用户态轻量线程 用户态库 / 运行时 通常共享
协程 语言级轻量执行流 语言运行时 通常共享
TCB 线程控制块 内核 / 线程库 不适用 用于管理线程
TLS 线程本地存储 运行时 / 编译器 / 系统支持 每线程一份变量实例 用于隔离线程变量

20. 本讲最重要的结论

本讲可以压缩成以下几句话:

  1. 进程是资源分配单位,线程是 CPU 执行和调度单位。
  2. 线程比进程轻,因为同一进程内线程共享代码、数据、堆和地址空间。
  3. 多线程共享地址空间,但不代表所有变量都共享。
  4. 每个线程有自己的栈、寄存器上下文和 TCB。
  5. TLS 让某些看似全局的变量变成每个线程一份。
  6. 用户态线程和内核态线程的区别在于谁管理、谁调度、内核是否看得见。
  7. 线程模型中的“对”表示用户态线程和内核态线程之间的映射关系。
  8. 多对一切换快但难以利用多核;一对一能利用多核但内核开销大;多对多理论灵活但实现复杂。
  9. pthread 的 create 分出执行流,join 等待执行流结束。
  10. 线程上下文切换的关键步骤是保存当前上下文、切换内核栈和页表、恢复目标上下文。
  11. 纤程和协程通过用户态调度降低切换成本,适合大量短任务和高并发场景。

结论

“进程与线程 II”的中心思想是执行流的层层轻量化。

最初,操作系统用进程管理程序运行。但进程太重,于是引入线程:线程共享进程资源,只保留独立执行所需的最小运行状态。随后,内核线程虽然能利用多核,但创建和切换仍有成本,于是用户态线程、纤程和协程继续把调度权上移到用户态或语言运行时。

这条主线可以总结为:

1
2
3
进程:拥有资源
线程:共享资源并执行代码
用户态线程 / 纤程 / 协程:更轻量地组织大量执行流

真正理解本讲的关键,不是背下每个名词,而是抓住三个层次:

  1. 资源层次:进程提供地址空间、代码、数据、堆和文件等资源。
  2. 执行层次:线程在进程资源之上独立执行,拥有自己的栈和上下文。
  3. 调度层次:内核线程由操作系统调度,用户态线程、纤程和协程由用户态运行时调度。

把这三个层次区分清楚,用户态线程、内核态线程、三种线程模型、TCB、TLS、pthread、上下文切换、纤程和协程就能连成一套完整机制。