进程与线程的区别(图解)

进程和线程是操作系统的核心概念,面试必问,理解它们对写好并发程序至关重要。

一、基本概念

进程(Process)

程序运行的实例,是资源分配的最小单位。

$ ps aux | grep nginx
root  1234  nginx: master process   ← 一个进程
www   1235  nginx: worker process   ← 另一个进程

线程(Thread)

进程内的执行单元,是 CPU 调度的最小单位。

一个进程可以包含多个线程,它们共享进程的资源。

┌─────────────────────────────────────┐
│            进程 (Process)            │
│  ┌─────────────────────────────┐    │
│  │      代码段 / 数据段          │    │
│  └─────────────────────────────┘    │
│  ┌─────────────────────────────┐    │
│  │      堆 (Heap)              │    │
│  └─────────────────────────────┘    │
│  ┌─────────┬─────────┬─────────┐    │
│  │ 线程 1   │ 线程 2   │ 线程 3   │    │
│  │ ┌─────┐ │ ┌─────┐ │ ┌─────┐ │    │
│  │ │ 栈  │ │ │ 栈  │ │ │ 栈  │ │    │
│  │ ├─────┤ │ ├─────┤ │ ├─────┤ │    │
│  │ │ PC  │ │ │ PC  │ │ │ PC  │ │    │
│  │ ├─────┤ │ ├─────┤ │ ├─────┤ │    │
│  │ │寄存器│ │ │寄存器│ │ │寄存器│ │    │
│  │ └─────┘ │ └─────┘ │ └─────┘ │    │
│  └─────────┴─────────┴─────────┘    │
└─────────────────────────────────────┘

二、核心区别对比

| 维度 | 进程 | 线程 |

| ---- | ---- | ---- |

| 资源 | 独立地址空间 | 共享进程资源 |

| 开销 | 创建/销毁开销大 | 创建/销毁开销小 |

| 切换 | 上下文切换慢 | 上下文切换快 |

| 通信 | IPC(管道、消息队列等) | 直接读写共享内存 |

| 安全性 | 一个进程崩溃不影响其他 | 一个线程崩溃整个进程崩溃 |

| 数量 | 通常几十到几百 | 可以成百上千 |

三、上下文切换

CPU 在多个进程/线程之间快速切换,给人「同时运行」的感觉。

进程切换

需要切换:

  • 虚拟地址空间(页表)
  • 内核栈
  • 寄存器、PC
  • TLB 失效

开销大:可能消耗几千个时钟周期。

线程切换

同一进程内的线程切换:

  • 不需要切换地址空间
  • 只切换寄存器、PC、栈
  • TLB 不失效

开销小:通常几百个时钟周期。

四、进程间通信(IPC)

由于进程内存隔离,需要专门的 IPC 机制:

| 方式 | 特点 | 场景 |

| ---- | ---- | ---- |

| 管道 (Pipe) | 半双工、有亲缘关系 | 父子进程 |

| 命名管道 (FIFO) | 任意进程 | 简单通信 |

| 消息队列 | 内核维护 | 异步通信 |

| 共享内存 | 最快 | 大数据量 |

| 信号量 | 同步互斥 | 锁机制 |

| 信号 | 异步通知 | 中断 |

| Socket | 跨网络 | 网络通信 |

共享内存示例

// 创建共享内存
int shmid = shmget(KEY, SIZE, IPC_CREAT | 0666);
char *addr = shmat(shmid, NULL, 0);

// 多进程读写同一块内存
strcpy(addr, "hello from 小宇宙");

五、线程间通信与同步

线程共享内存,通信简单,但需要同步防止数据竞争:

// 线程不安全
class Counter {
    int count = 0;
    public void inc() {
        count++;  // 非原子操作
    }
}

// 加锁后安全
class Counter {
    int count = 0;
    synchronized public void inc() {
        count++;
    }
}

同步方式

  • 互斥锁(Mutex):保护临界区
  • 读写锁:读多写少场景
  • 条件变量:等待通知
  • 信号量(Semaphore):控制并发数
  • 自旋锁:短临界区

六、协程(Coroutine)

更轻量级的「用户态线程」。

| 维度 | 进程 | 线程 | 协程 |

| ---- | ---- | ---- | ---- |

| 调度 | 内核 | 内核 | 用户态 |

| 切换开销 | 大 | 中 | 小 |

| 数量 | 几十 | 几百/千 | 几万/十万 |

| 编程模型 | 阻塞 | 阻塞 | 同步非阻塞 |

Go 语言的 goroutine

// 启动一个 goroutine(协程)
go func() {
    fmt.Println("小宇宙在 goroutine 中")
}()

Go 的 GMP 调度器让成百万 goroutine 高效运行。

七、实战:多线程问题

1. 竞态条件

// 1000 个线程各自对 count++,结果不是 1000
ExecutorService pool = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
    pool.submit(() -> counter.inc());
}
// count 可能是 9xx

2. 死锁

// 线程 1:持有 A,请求 B
// 线程 2:持有 B,请求 A
synchronized(lockA) {
    synchronized(lockB) { ... }
}

死锁四个必要条件

  1. 互斥
  2. 持有并等待
  3. 不可剥夺
  4. 循环等待

3. 内存可见性

// 没用 volatile,子线程可能永远读不到 stop 的新值
boolean stop = false;

while (!stop) {
    // ...
}

八、面试高频题

Q1:线程和进程的区别?

一句话:进程是资源分配的最小单位,线程是 CPU 调度的最小单位

详细:进程独立内存,线程共享进程内存;进程开销大,线程开销小。

Q2:多线程一定比单线程快吗?

不一定。

  • IO 密集型:多线程快(IO 时 CPU 可以做别的)
  • CPU 密集型:线程数 > CPU 核数反而更慢(切换开销)

Q3:协程的优势?

  • 用户态调度,开销极小
  • 单机可创建海量协程
  • 同步编程模型,更易理解

Q4:如何避免死锁?

  • 破坏四个必要条件之一
  • 固定加锁顺序
  • 使用 tryLock + 超时
  • 死锁检测

九、小结

| 概念 | 一句话 |

| ---- | ---- |

| 进程 | 资源分配单位 |

| 线程 | CPU 调度单位 |

| 协程 | 用户态轻量级线程 |

| 上下文切换 | 进程切换 > 线程切换 |

| IPC | 进程间通信 |

| 同步 | 线程间协作 |

小宇宙提醒:并发编程的本质是协调多个执行流对共享资源的访问,掌握锁和通信是关键。