面试官:请问进程、线程、协程的区别是什么?

面试回答

“简单来说,进程是资源分配的最小单位,线程是 CPU 调度的最小单位,而协程是用户态的轻量级线程。

进程拥有独立的内存空间和系统资源,进程之间相互隔离,安全性高,但创建、销毁和切换的开销很大。

线程是进程内的一个执行流,同一个进程内的多个线程共享内存和资源。线程的创建和切换开销比进程小,但涉及内核态的切换,依然有一定成本。此外,多线程并发时需要通过锁等机制处理同步问题。

协程完全由用户程序(如 Go 的 runtime)管理,不经过操作系统内核,因此也被称为用户态线程。它的创建和切换开销极小,可以轻松创建十万、百万级别。在 Go 语言中,Goroutine 就是协程的典型实现。

在实际选型中,如果是 CPU 密集型任务,通常使用多进程或多线程来利用多核优势;如果是 I/O 密集型的高并发场景,使用协程能带来最大的性能收益。”

系统讲解

核心对比

特性进程 (Process)线程 (Thread)协程 (Coroutine)
定义资源分配的最小单位CPU 调度的最小单位用户态的轻量级线程
资源开销极大(独占内存空间、文件描述符等)较小(共享进程资源,独占栈空间)极小(初始栈通常仅为 2KB 左右)
切换成本极高(涉及页表切换、TLB 失效等内核操作)中等(无需页表切换,需陷入内核态保存寄存器极低(完全在用户态进行寄存器保存与恢复)
并发性低(受限于系统资源)中(受限于内核线程数上限及切换开销)极高(可轻松支撑百万级并发)
数据同步复杂(IPC:管道、共享内存、消息队列等)需注意安全(共享内存,依赖互斥锁、条件变量等)简单(通常通过 Channel 或类似机制进行通信)
崩溃影响隔离性好,不影响其他进程一个线程崩溃可能导致整个进程崩溃一个协程 Panic 未捕获会导致整个进程崩溃

亮点与深度

上下文切换的底层开销

  • 进程切换:不仅需要保存和恢复 CPU 寄存器上下文,最致命的是需要切换虚拟内存地址空间(切换页表),这会导致 TLB(Translation Lookaside Buffer,快表)被刷出,从而引发大量的缓存未命中(Cache Miss),大幅降低运行效率。
  • 线程切换:因为共享了进程的虚拟内存空间,所以不需要切换页表,TLB 依然有效。但仍然需要从用户态陷入内核态,保存和恢复 CPU 的各种寄存器。
  • 协程切换:完全在用户层通过代码模拟,只需要保存极少数关键的寄存器(如 PC 程序计数器、SP 栈指针等)即可,通常只需几纳秒,没有系统调用的开销。

协程的工程落地:Goroutine

Goroutine 是协程在工程上最成功的落地之一。Go 语言自带了一个非常强大的调度器(GMP 模型),它负责将成千上万个轻量级的 Goroutine 动态地映射到少量的操作系统线程(M)上执行。

当某个 Goroutine 发生 I/O 阻塞时,调度器会自动将其剥离出线程,并把该线程分配给其他就绪的 Goroutine,从而实现极致的并发榨取。

代码示例

这里用 Go 语言展示如何以极低的成本创建百万协程:

package main
 
import (
	"fmt"
	"time"
)
 
func main() {
	start := time.Now()
	
	// 启动 100 万个协程
	for i := 0; i < 1000000; i++ {
		go func() {
			time.Sleep(10 * time.Second) // 模拟阻塞任务
		}()
	}
 
	fmt.Printf("成功启动了 100 万个 Goroutine,耗时: %v\n", time.Since(start))
	// 输出通常不到 1 秒
}

注:如果在 Java 或 C++ 中创建 100 万个操作系统级别的线程,几乎必定会导致内存耗尽(OOM)或直接崩溃。