从调度模型到内存布局,彻底厘清并发体系的内在逻辑


一、并发的基本问题

并发编程的核心目标是在有限的计算资源上,通过时间复用或空间复用,最大化任务的吞吐量。操作系统提供了两种基本抽象——进程线程,而编程语言在此基础上发展出协程这一用户态调度机制。理解三者的本质区别,首先需要明确调度权的归属和资源隔离的边界。


二、进程(Process):操作系统资源管理的边界

2.1 定义

进程是操作系统进行资源分配的基本单位。每个进程拥有独立的虚拟地址空间、文件描述符表、信号处理表以及进程控制块(PCB)。进程之间彼此隔离,一个进程的崩溃不会直接影响其他进程。

2.2 内存布局

text

进程虚拟地址空间:
┌───────────────────────┐ 高地址
│       栈 (Stack)       │ ← 函数调用帧
├───────────────────────┤
│       堆 (Heap)        │ ← 动态分配(malloc)
├───────────────────────┤
│     数据段 (Data)      │ ← 全局变量、静态变量
├───────────────────────┤
│     代码段 (Text)      │ ← 可执行指令
└───────────────────────┘ 低地址

每个进程的地址空间完全独立,一个进程无法直接访问另一个进程的内存,这种隔离机制是操作系统安全性的基石。

2.3 进程间通信(IPC)

由于地址空间隔离,进程间通信必须通过操作系统提供的特殊机制:

机制 特点
管道(Pipe) 单向、亲缘进程间
命名管道(FIFO) 任意进程间
消息队列(Message Queue) 异步、消息边界
共享内存(Shared Memory) 最高效、需同步
信号(Signal) 异步事件通知
Socket 跨网络、双向

2.4 切换开销

进程上下文切换涉及:

  • 页表切换(TLB 刷新)

  • 寄存器保存与恢复

  • 内核态与用户态切换

  • 文件描述符等资源重定位

因此,进程切换属于重量级操作,典型开销在微秒级到毫秒级之间。


三、线程(Thread):操作系统调度的基本单位

3.1 定义

线程是操作系统进行CPU 调度的基本单位。同一进程内的多个线程共享该进程的地址空间、文件描述符等资源,但各自拥有独立的栈空间、程序计数器和寄存器状态。

3.2 线程模型

text

进程 P
├── 共享资源
│   ├── 堆内存
│   ├── 全局变量
│   └── 文件描述符
├── 线程 T1
│   ├── 线程栈(独立)
│   └── TCB(线程控制块)
├── 线程 T2
│   ├── 线程栈(独立)
│   └── TCB(线程控制块)
└── 线程 T3
    ├── 线程栈(独立)
    └── TCB(线程控制块)

3.3 内核态线程与用户态线程

类型 调度者 特点
内核态线程(Kernel Thread) 操作系统内核 真正的并行,可被调度到不同 CPU 核心
用户态线程(User Thread) 用户态库 不涉及系统调用,切换快,但无法利用多核

Linux 的 clone() 系统调用本质上创建的是内核态线程,而 pthread 库是对其的封装。Python 的 threading 模块是对 POSIX 线程的封装,因此属于内核态线程。

3.4 线程同步机制

由于线程共享内存空间,对共享变量的并发访问需要同步原语:

原语 适用场景
互斥锁(Mutex) 互斥访问临界区
读写锁(RWLock) 读多写少
条件变量(Condition) 条件等待/通知
信号量(Semaphore) 资源计数
自旋锁(Spinlock) 临界区极短

3.5 线程切换开销

线程切换不需要切换页表和地址空间,仅需保存/恢复寄存器状态和栈指针,属于轻量级操作。但仍涉及用户态与内核态的切换,开销低于进程切换。


四、协程(Coroutine):用户态调度的控制流

4.1 定义

协程是一种用户态的并发原语,由编程语言的运行时库进行调度,而非操作系统。协程通过主动让出(yield/await)控制权来实现协作式多任务,而非抢占式。

4.2 协程的核心特征

特征 说明
协作式调度 协程主动让出执行权,不由操作系统抢占
用户态切换 不涉及系统调用,仅保存和恢复栈帧
极低开销 切换仅需数十纳秒,远低于线程切换
单线程内并发 一个线程内可运行数万个协程
无数据竞争 单线程内不存在并行写冲突

4.3 协程的实现机制

协程的实现依赖于状态机事件循环

text

┌─────────────────────────────────────────────────────┐
│                    事件循环                         │
│  ┌─────────────────────────────────────────────┐   │
│  │  就绪队列                                   │   │
│  │  ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐         │   │
│  │  │ 协程 │ │ 协程 │ │ 协程 │ │ 协程 │         │   │
│  │  │  A   │ │  B   │ │  C   │ │  D   │         │   │
│  │  └─────┘ └─────┘ └─────┘ └─────┘         │   │
│  └─────────────────────────────────────────────┘   │
│           │                       │                 │
│           ▼                       ▼                 │
│      执行协程 A             等待 I/O 完成           │
│      await 让出           I/O 完成 → 加入就绪队列   │
└─────────────────────────────────────────────────────┘

4.4 Python 的协程模型

Python 3.5+ 通过 async/await 语法提供对协程的原生支持:

python

async def fetch(url):
    # await 触发协程切换,让出 CPU
    response = await aiohttp.get(url)
    return response

await 关键字是协程主动让出的信号,当执行到 I/O 操作时,协程将控制权交回事件循环,由事件循环调度其他就绪协程。


五、调度模型对比

维度 进程 线程 协程
调度者 操作系统内核 操作系统内核 用户态运行时
调度方式 抢占式 抢占式 协作式
上下文切换 内核态切换 内核态切换 用户态切换
切换开销 微秒级 微秒级 纳秒级
并发上限 数百 数千 数十万
资源隔离 完全隔离 共享 共享
内存占用 MB 级 MB 级 KB 级

六、GIL 对并发模型的影响

CPython 的实现中存在全局解释器锁(Global Interpreter Lock)。该锁保证同一时刻只有一个线程执行 Python 字节码。因此,Python 的多线程在 CPU 密集型任务中无法利用多核优势,但在 I/O 密集型任务中,线程因等待 I/O 而释放 GIL,依然可以获得并发收益。

任务类型 进程 线程 协程
CPU 密集型 ✅ 真并行(多核) ❌ GIL 限制 ❌ 单线程串行
I/O 密集型 ✅ 最优
高并发 I/O ⚠️ 线程数受限 ✅ 最优

七、适用场景归纳

进程

  • CPU 密集型计算(加密、压缩、图像处理)

  • 需要进程级隔离的容器化任务

  • 对稳定性要求极高的场景(一个进程崩溃不影响其他进程)

线程

  • I/O 密集型任务(文件读写、网络请求、数据库查询)

  • 需要共享数据的并发任务

  • 并发数量可控的场景(通常不超过数千个线程)

协程

  • 极高并发 I/O 场景(数十万并发连接)

  • 网络爬虫、代理服务器、实时消息推送

  • 需要极低响应延迟的服务


八、结论

进程、线程、协程构成了并发编程的三级抽象体系,每一级都在调度权、资源隔离与开销之间做了不同的权衡:

  • 进程以最高的资源开销换取最高的隔离性

  • 线程在共享内存空间中实现并发执行

  • 协程以用户态调度换取极致的并发密度

三者并非互斥关系,而是可以协同使用:一个进程可以包含多个线程,每个线程可以运行多个协程。正确的做法是根据任务特征选择合适的抽象层级。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐