进程、线程、协程
2025/12/21大约 6 分钟
进程、线程、协程
- 进程、线程、协程
基本定义
进程(Process)
- 进程是资源分配的基本单位,是操作系统进行资源分配和调度的独立单位
- 每个进程都有独立的地址空间、内存、数据栈以及其他用于追踪执行的辅助数据
- 进程间相互独立,一个进程崩溃不会影响其他进程
线程(Thread)
- 线程是CPU调度的基本单位,是进程中的一个执行单元
- 同一进程内的多个线程共享进程的地址空间、内存、文件句柄等资源
- 线程有自己的程序计数器、寄存器集合和栈
协程(Coroutine)
- 协程是一种用户态的轻量级线程,由用户程序自己控制调度
- 协程的切换不需要内核参与,完全由程序控制,开销极小
- 协程在一个线程内执行,通过主动让出CPU来实现调度
详细对比
1. 调度方式
- 进程:由操作系统内核进行调度,调度开销大(需要切换页表、刷新TLB等)
- 线程:由操作系统内核进行调度,但开销比进程小(共享地址空间,不需要切换页表)
- 协程:由用户程序自己调度,无需内核参与,调度开销最小
2. 切换开销
- 进程切换:开销最大(1000-1500个CPU周期)
- 需要切换页表
- 切换内核态和用户态
- 刷新TLB(Translation Lookaside Buffer)
- 保存和恢复上下文(寄存器、程序计数器等)
- 线程切换:开销中等(100-200个CPU周期)
- 不需要切换页表(共享地址空间)
- 需要切换内核态和用户态
- 保存和恢复上下文
- 协程切换:开销最小(几十个CPU周期)
- 完全在用户态进行
- 只需保存和恢复少量寄存器
- 无需系统调用
3. 内存占用
- 进程:每个进程独立的地址空间,内存占用大(MB级别)
- 线程:共享进程地址空间,但每个线程需要独立的栈空间(通常1-8MB)
- 协程:共享线程栈空间或使用小栈(通常几KB到几十KB),内存占用最小
4. 通信方式
- 进程间通信(IPC):
- 管道(Pipe)
- 信号(Signal)
- 消息队列(Message Queue)
- 共享内存(Shared Memory)
- 信号量(Semaphore)
- 套接字(Socket)
- 开销大,需要内核参与
- 线程间通信:
- 共享内存(全局变量、堆内存)
- 互斥锁(Mutex)
- 条件变量(Condition Variable)
- 信号量(Semaphore)
- 通信简单但需要同步机制
- 协程间通信:
- Channel(Go语言)
- 共享变量
- 由于单线程执行,无需加锁
5. 并发性
- 进程:真正的并行执行(多核CPU上)
- 线程:真正的并行执行(多核CPU上)
- 协程:在单线程内并发执行,不是并行(除非在多个线程上分别运行协程)
6. 资源占用
- 进程:系统可创建的进程数量有限(通常几千到几万)
- 线程:系统可创建的线程数量受限于内存(通常几千到几万)
- 协程:可创建数量非常大(可达百万级别)
7. 崩溃影响
- 进程:一个进程崩溃不影响其他进程,隔离性最好
- 线程:一个线程崩溃可能导致整个进程崩溃
- 协程:一个协程异常可能影响整个线程
使用场景
进程适用场景:
- 需要高度隔离和安全性的场景
- 多核CPU密集型任务,需要真正的并行计算
- 需要利用多机资源的分布式系统
- 浏览器的多标签页隔离
线程适用场景:
- 需要共享大量数据的并发任务
- IO密集型任务(网络请求、文件读写)
- 需要真正并行计算的多核任务
- GUI应用程序(主线程处理界面,工作线程处理任务)
协程适用场景:
- 高并发IO密集型任务(网络服务器、爬虫)
- 需要大量并发连接的场景(C10K/C10M问题)
- 异步编程模型
- 状态机实现
- 生产者-消费者模型
代码示例对比
Golang中的进程、线程、协程
package main
import (
"fmt"
"os"
"os/exec"
"sync"
"time"
)
// 1. 多进程示例
func multiProcessExample() {
// 创建多个子进程
for i := 0; i < 5; i++ {
cmd := exec.Command("echo", fmt.Sprintf("Worker %d", i))
cmd.Stdout = os.Stdout
if err := cmd.Start(); err != nil {
fmt.Println("Error:", err)
continue
}
// 等待进程结束
cmd.Wait()
}
}
// 2. 多线程示例(Goroutine绑定到OS线程)
func multiThreadExample() {
var wg sync.WaitGroup
// 使用GOMAXPROCS设置使用的OS线程数
// runtime.GOMAXPROCS(runtime.NumCPU())
for i := 0; i < 5; i++ {
wg.Add(1)
go func(num int) {
defer wg.Done()
// runtime.LockOSThread() // 锁定到特定OS线程
fmt.Printf("Worker %d\n", num)
// runtime.UnlockOSThread()
}(i)
}
wg.Wait()
}
// 3. 协程示例(Goroutine - Go的原生协程)
func coroutineExample() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(num int) {
defer wg.Done()
fmt.Printf("Worker %d\n", num)
time.Sleep(1 * time.Second)
}(i)
}
wg.Wait()
}
// 协程通信示例(使用Channel)
func coroutineWithChannel() {
ch := make(chan int, 5)
var wg sync.WaitGroup
// 生产者协程
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < 5; i++ {
ch <- i
}
close(ch)
}()
// 消费者协程
wg.Add(1)
go func() {
defer wg.Done()
for num := range ch {
fmt.Printf("Worker %d\n", num)
}
}()
wg.Wait()
}
func main() {
fmt.Println("=== 多进程示例 ===")
multiProcessExample()
fmt.Println("\n=== 多线程示例 ===")
multiThreadExample()
fmt.Println("\n=== 协程示例 ===")
coroutineExample()
fmt.Println("\n=== 协程通信示例 ===")
coroutineWithChannel()
}Goroutine的特点:
- Goroutine是Go语言内置的协程实现,由Go运行时调度
- 初始栈大小只有2KB,可动态增长
- Go运行时使用M:N调度模型,将M个Goroutine调度到N个OS线程上
- 通过Channel进行通信,遵循"不要通过共享内存来通信,而应该通过通信来共享内存"的理念
性能对比总结
| 特性 | 进程 | 线程 | 协程 |
|---|---|---|---|
| 调度单位 | 操作系统 | 操作系统 | 用户程序 |
| 切换开销 | 最大(ms级) | 中等(μs级) | 最小(ns级) |
| 内存开销 | 最大(MB级) | 中等(MB级) | 最小(KB级) |
| 创建销毁 | 慢 | 较快 | 很快 |
| 数量上限 | 低(千级) | 中(万级) | 高(百万级) |
| 并行性 | 支持 | 支持 | 不支持(单线程) |
| 隔离性 | 强 | 弱 | 弱 |
| 通信开销 | 大 | 小 | 极小 |
| 适用场景 | CPU密集+隔离 | CPU密集+共享 | IO密集+高并发 |
关键点总结
- 进程是资源分配单位,线程是调度单位,协程是编程模型
- 开销:进程 > 线程 > 协程
- 隔离性:进程 > 线程 > 协程
- 并发数量:协程 > 线程 > 进程
- CPU密集型任务选进程或线程,IO密集型任务选协程
- 需要真正并行选进程或线程,高并发异步选协程