Skip to content

进程、线程、协程区别

1. 核心结论

进程是资源分配单位,线程是 CPU 调度单位,协程是用户态调度的轻量执行单元。

维度 进程 线程 协程
调度者 OS 内核 OS 内核 用户态运行时或事件循环
资源隔离 强,独立地址空间 弱,共享进程地址空间 更弱,通常共享线程上下文
上下文切换 最重,涉及地址空间切换 中等,涉及内核线程切换 最轻,通常不进入内核
并行能力 可多核并行 可多核并行 单个线程内不能真正并行,多线程运行时可并行
通信方式 IPC、管道、Socket、共享内存 共享内存、锁、队列 Channel、await、事件循环、调度器
典型问题 IPC 成本高 锁竞争、死锁、数据竞争 阻塞调用、调度饥饿、回调链路复杂

2. 进程

2.1 基础定义

进程是操作系统对一个正在运行程序的抽象。它拥有独立的 虚拟地址空间、文件描述符、信号处理器、权限、内存映射等资源。

可以粗略理解为:

进程 = 资源容器 + 至少一个线程

2.2 核心资源

一个进程通常包含:

  • 代码段:程序指令。

  • 数据段:全局变量、静态变量。

  • :动态分配内存,例如 mallocnew

  • :线程调用栈,注意栈本质上属于线程。

  • 文件描述符表:打开的文件、Socket。

  • 虚拟地址空间:通过 MMU 映射到物理内存。

  • PCB:Process Control Block,内核维护的进程控制块。

2.3 特点

进程之间默认内存隔离,一个进程崩溃通常不会直接破坏另一个进程的地址空间。

但进程切换成本较高,因为可能涉及:

  • 页表切换。

  • TLB 失效。

  • 内核态切换。

  • CPU 缓存局部性下降。

3. 线程

3.1 基础定义

线程是进程内部的执行流,也是主流操作系统中实际参与 CPU 调度的基本单位。

多个线程共享同一个进程的:

  • 堆内存。

  • 全局变量。

  • 文件描述符。

  • 代码段。

  • 进程权限。

但每个线程拥有自己的:

  • 栈。

  • 寄存器上下文。

  • 程序计数器。

  • TCB,Thread Control Block。

3.2 线程切换

线程上下文切换通常需要保存和恢复:

寄存器
程序计数器
栈指针
线程状态
调度优先级

线程切换比进程轻,但仍然需要内核参与,所以成本高于协程。

3.3 线程的主要问题

线程共享内存,因此并发访问必须处理同步问题:

  • 数据竞争。

  • 锁竞争。

  • 死锁。

  • 活锁。

  • 伪共享。

  • 内存可见性问题。

例如 Java 中需要通过 synchronizedvolatileLockCASAQS 等机制保证线程安全。

class Counter {
    private int count = 0;

    public synchronized void incr() {
        count++; // 注意此处需要加锁,否则多个线程并发写会丢失更新
    }
}

4. 协程

4.1 基础定义

协程是用户态的轻量执行单元。它通常不直接由操作系统调度,而是由语言运行时、事件循环或协程调度器调度。

可以粗略理解为:

协程 = 可挂起、可恢复的函数执行上下文

协程的关键能力是:

  • 执行到某个点可以 挂起

  • 之后可以从挂起点 恢复

  • 挂起和恢复一般不需要进入内核态。

4.2 协程与线程的关系

协程通常运行在线程之上:

进程
└── 线程
    ├── 协程 A
    ├── 协程 B
    └── 协程 C

如果只有一个线程承载多个协程,那么这些协程是 并发,不是 并行

如果运行时使用多个线程承载多个协程,例如 Go 的 GMP 模型,则多个协程可以在多核上并行执行。

4.3 有栈协程与无栈协程

类型 说明 代表
有栈协程 每个协程有独立调用栈,可以在深层调用中挂起 Go goroutine、Lua coroutine
无栈协程 编译器把函数改写成状态机,挂起点通常受语法限制 JavaScript async/await、Rust async、C++20 coroutine、Kotlin coroutine

4.4 协程为什么轻量

协程轻量主要来自三点:

  1. 用户态调度:切换不必频繁进入内核。

  2. 栈更小或按需增长:例如 Go goroutine 初始栈很小,可动态扩容。

  3. 挂起点明确:通常在 awaityield、Channel 阻塞、IO 等位置切换。

但协程不是银弹。若协程内部执行阻塞系统调用,可能阻塞底层线程,导致整个调度器吞吐下降。

5. 三者关系

5.1 层级模型

操作系统
└── 进程:资源隔离边界
    └── 线程:内核调度单位
        └── 协程:用户态调度单位

5.2 并发与并行

概念 含义
并发 多个任务在一段时间内交替推进
并行 多个任务在同一时刻真正同时执行
异步 发起操作后不阻塞当前执行流,结果未来返回
非阻塞 调用立即返回,不等待资源就绪
多路复用 一个线程监听多个 IO 事件,例如 epollkqueueIOCP

协程通常用于提升 高并发 IO 场景 的吞吐,例如网关、爬虫、RPC 服务、IM 服务。

CPU 密集任务仍然需要多进程或多线程并行利用多核。

6. 调度模型

6.1 1:1 模型

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

User Thread 1 -> Kernel Thread 1
User Thread 2 -> Kernel Thread 2

典型代表:

  • Java 传统 Thread

  • C/C++ pthread

  • Rust std::thread

  • Python threading,但受 GIL 影响。

优点是模型简单,可以利用多核。

缺点是线程数量过多时,内核调度成本高。

6.2 N:1 模型

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

Coroutine 1
Coroutine 2  -> Kernel Thread 1
Coroutine 3

典型代表:

  • JavaScript 主线程事件循环。

  • Python asyncio 默认单线程事件循环。

优点是切换成本低。

缺点是一个阻塞调用可能阻塞整个线程,且单线程内不能真正并行。

6.3 M:N 模型

多个协程映射到多个内核线程。

Coroutine N -> Runtime Scheduler -> Kernel Thread M

典型代表:

  • Go goroutine。

  • Java Virtual Thread,底层由 ForkJoinPool 承载。

  • Erlang lightweight process。

  • 部分 Fiber 运行时。

优点是兼顾轻量级和多核并行。

缺点是运行时调度器复杂,需要处理阻塞、抢占、栈管理、IO 事件集成。

7. 不同语言实现思路

7.1 Java

Java 传统 Thread 基本是 1:1 内核线程模型

Thread t = new Thread(() -> {
    System.out.println("run");
});
t.start();

简化调用链:

Thread.start()
 -> start0()
 -> JVM native
 -> OS create thread

传统 Java 线程适合 CPU 密集和普通并发,但大量线程会带来较高内存与调度成本。

Java 21 引入正式版 Virtual Thread,属于轻量线程模型。它不是直接等价于 OS 线程,而是由 JVM 调度到少量 carrier thread 上运行。

Thread.startVirtualThread(() -> {
    // 阻塞式写法,底层可由 JVM 挂起虚拟线程
    doIo();
});

核心思路:

Virtual Thread -> JVM Scheduler -> Carrier Thread -> OS Thread

它的目标是让开发者继续写阻塞式代码,同时获得接近协程的高并发能力。

7.2 Go

Go 使用 goroutine,核心是 GMP 调度模型

组件 含义
G goroutine,用户态执行单元
M machine,内核线程
P processor,调度上下文,持有本地运行队列

模型:

G -> P -> M -> CPU

Go 的特点:

  • goroutine 初始栈较小,可动态扩缩。

  • runtime 调度器支持抢占。

  • 网络 IO 与 runtime netpoller 集成。

  • Channel 是主要通信机制。

go func() {
    // 新建一个 goroutine
    fmt.Println("hello")
}()

Go 的实现思路不是简单事件循环,而是运行时层面的 M:N 调度。

7.3 Python

Python 有两套常见并发模型。

第一种是 threading,使用 OS 线程,但 CPython 有 GIL

Python Thread -> OS Thread

GIL 使得同一时刻通常只有一个线程执行 Python 字节码。因此 Python 多线程适合 IO 密集,不适合纯 Python CPU 密集并行。

第二种是 asyncio,典型无栈协程加事件循环:

import asyncio

async def task():
    await asyncio.sleep(1)  # 注意此处挂起协程,不阻塞整个线程

asyncio.run(task())

核心思路:

async def -> coroutine object
await -> yield control
event loop -> resume coroutine

7.4 JavaScript

JavaScript 主流运行环境采用 单线程事件循环 + Promise 微任务队列

async function task() {
  await fetch("/api"); // 注意此处挂起 async 函数,控制权返回事件循环
}

核心结构:

Call Stack
Event Loop
Macro Task Queue
Micro Task Queue
Web APIs / libuv

浏览器和 Node.js 都可以通过底层线程池处理部分 IO 或计算,但 JS 主执行栈通常是单线程。

所以 JavaScript 的 async/await 本质上不是创建线程,而是把异步流程改写成 Promise 状态机。

7.5 Kotlin

Kotlin coroutine 是典型的 无栈协程 + 编译器状态机 + Dispatcher 调度

suspend fun task() {
    delay(1000) // 注意此处挂起协程,不阻塞线程
}

核心思路:

suspend function -> Continuation
挂起点 -> 保存状态
Dispatcher -> 选择线程恢复执行

Kotlin 的协程不是 OS 线程,它可以运行在不同的 Dispatcher 上,例如:

  • Dispatchers.Default:CPU 密集。

  • Dispatchers.IO:阻塞 IO。

  • Dispatchers.Main:UI 线程。

7.6 Rust

Rust async无栈协程 + Future + Executor poll 模型

async fn task() {
    do_io().await; // 注意此处生成状态机,由 executor 驱动
}

核心思路:

async fn -> Future
Future::poll()
Poll::Pending -> 挂起
Waker -> 唤醒
Executor -> 再次 poll

Rust 标准库定义了 Future 抽象,但不内置完整运行时。实际常用运行时包括 Tokio、async-std 等。

7.7 C++20

C++20 coroutine 是语言层面的协程机制,但标准库没有提供统一调度器。

核心思路:

coroutine function
 -> coroutine frame
 -> promise_type
 -> co_await / co_yield / co_return

C++20 提供的是底层构建块,真正的事件循环、调度器、IO 集成通常由框架实现。