深蓝黑背景封面。左侧示意图:两个圆角方框分别代表进程 A 与进程 B,各自拥有独立地址空间;进程 A 框内上方是「代码 · 数据 · 堆(进程独占)」区域、下方一个青色描边的「线程:私有栈+寄存器」小块;进程 B 框用紫色描边,框内上方同样是「代码 · 数据 · 堆」区域但标注「三线程共享」,下方并排三个青色小块「线程 1 / 线程 2 / 线程 3」,各自带私有栈。方框下方三行注释:进程之间地址空间隔离互不越界;进程之内线程共享堆、各有一叠栈;进程是资源分配单位、线程是 CPU 调度单位。右侧大标题「进程和线程,到底差在哪」,副标题「多线程真的能节省时间吗?」,小字「CPU 密集与 IO 密集,收益边界不一样」。左上标注「面试经典 · 操作系统」,右上圆角胶囊标注「Day 02 · 11-09」 图 1:本文封面,进程各守一块地址空间,线程在进程内部共享堆、各持一叠栈。

先说明属性:这是一篇经典题解析,题目出自本地历史面试资料,与任何公司的当前招聘无关。这道题其实是两问:概念上进程和线程差在哪;工程上多线程什么时候真的省时间。第一问多数人会背,第二问才是拉开差距的地方。

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

程序跑起来,操作系统给它划一块独立的地址空间——代码段、数据段、堆、打开的文件,这份「家产」挂在进程名下。线程不领家产,它领的是一份「执行现场」:一组寄存器、程序计数器、一叠私有的栈。一个进程可以养多个线程,它们共享进程的全部家产,各跑各的执行现场。

Linux 的 pthreads 手册把这层关系写得很白:同一进程内的线程共享全局内存(数据段与堆)、进程 ID、打开的文件描述符、信号处理方式;而每个线程私有的是栈、线程 ID、errno 变量与信号掩码。共享与私有的清单,就是这道题的骨架。

五个维度把区别钉死。

地址空间:进程之间相互独立,A 写不坏 B 的内存;线程共享所属进程的堆与全局区,方便,但任何线程都能改写共享数据。创建与切换:进程要建地址空间、换页表,开销大;线程只换栈与寄存器现场,开销小得多。通信:进程走 IPC(管道、消息队列、共享内存);线程直接读写共享变量——但必须同步,否则就是数据竞争。崩溃影响:一个进程崩溃通常不拖累别人;一个线程段错误,整个进程一起死。角色定位:进程管资源,线程管执行。

第二问之 CPU 密集:核数就是天花板。

多线程省时间的前提,是有可并行的事、且有闲置的核。笔者在本机(10 个逻辑核)用 Node.js 的 worker_threads(真并行线程)实测:每个线程固定做 2 亿次开方运算,1 个线程 119ms,8 个线程 165ms——墙钟几乎不涨,因为核还够分,总吞吐翻了约 8 倍;加到 16 个线程,墙钟立刻涨到 298ms,超过核数后线程开始排队,再多线程也变不出 CPU。结论:CPU 密集任务的加速比上限就是核数,线程开多了只剩切换开销。

这里有个面试常追问的点:CPython 有 GIL(全局解释器锁),同一时刻只允许一个线程执行 Python 字节码,所以 Python 里 CPU 密集的多线程不但不提速还可能更慢,正确姿势是多进程;IO 密集则不受影响,因为线程等待时 GIL 会释放。

第二问之 IO 密集:等待时间可以被重叠。

另一类任务的瓶颈不在算,而在等:等网络、等磁盘、等下游接口。一个线程等待时 CPU 闲着,另一个线程正好顶上。同样实测:8 个任务各含 10ms 计算加 90ms 模拟 IO 等待,串行跑完 805ms,8 路并发只要 169ms——灰色的等待段被别的线程的计算填满了。边界同样存在:线程数超过「核数加 IO 并发上限」后收益递减,还要付切换与每线程一叠栈的内存成本。

实测代码。

const { Worker, isMainThread, workerData, parentPort } = require('node:worker_threads');
const os = require('node:os');
function work(n) { let x = 0; for (let i = 1; i <= n; i++) x += Math.sqrt(i); return x; }
if (!isMainThread) { parentPort.postMessage(work(workerData.n)); }
else (async () => {
  const spawn = t => Promise.all(Array.from({ length: t }, () =>
    new Promise(r => { const w = new Worker(__filename, { workerData: { n: 2e8 } }); w.on('message', r); })));
  console.log('逻辑核数:', os.cpus().length);
  for (const t of [1, 8, 16]) {
    const t0 = Date.now(); await spawn(t);
    console.log(`CPU 密集: ${t} 个线程各算 2 亿次 sqrt → 墙钟 ${Date.now() - t0}ms`);
  }
  const io = async (par) => { // 8 个任务,各 10ms 计算 + 90ms 等待
    const t0 = Date.now(); let i = 0;
    const busy = ms => { const e = Date.now() + ms; while (Date.now() < e); };
    const runner = async () => { while (i < 8) { i++; busy(10); await new Promise(r => setTimeout(r, 90)); } };
    await Promise.all(Array.from({ length: par }, runner));
    return Date.now() - t0;
  };
  console.log(`IO 密集: 串行 ${await io(1)}ms → 8 路并发 ${await io(8)}ms`);
})();

上方一张三列五行对比表:地址空间(进程间相互独立互不越界;线程共享所属进程的堆与全局区)、创建与切换(进程开销大,要切换地址空间与页表;线程开销小,只换栈与寄存器现场)、通信方式(进程走 IPC:管道、消息队列、共享内存;线程直接读写共享变量但必须加锁)、崩溃影响(一个进程崩溃不拖累其他进程;一个线程出错可能拖垮整个进程)、角色定位(进程是资源分配的基本单位;线程是 CPU 调度的基本单位)。左下面板「CPU 密集:核数就是天花板」是吞吐量随线程数变化的折线图:折线先线性上升,到达「核数」虚线后转为水平,注释说明线程数超过核数不再提速、Python 受 GIL 限制时 CPU 密集场景要用多进程才能吃满多核。右下面板「IO 密集:等待时间可被重叠」用两条水平时间条对比:上面「串行」条由青色计算段与灰色 IO 等待段交替组成长度 T;下面「双线程重叠」条明显更短约 0.6 T,注释说明灰色等待段里另一线程的计算正在填充 CPU,但超过「核数+IO 并发上限」后收益递减 图 2:五维对比表与两类场景的收益边界。

回到面试现场,建议的答题结构:先给一句话分野(资源分配 vs CPU 调度),再用「共享什么、私有什么」展开五个维度,然后主动把第二问拆成两个场景——CPU 密集看核数天花板(可顺势提 GIL 与多进程对策),IO 密集看等待重叠(收益递减有边界)。能说出边界条件,背诵题就答成了工程题。最后补一句选型经验:要隔离与健壮性选多进程,要低开销共享数据选多线程。你的项目用的是多进程还是多线程架构,当时怎么权衡的?评论区聊聊。