本文从一个最朴素的问题出发:CPU 单核到底是怎么"同时"处理 100 个请求的?读完你会彻底搞懂"并发"和"并行"——以及为什么新手最容易把它们搞混。
一、先纠正一句话
很多人这样开头:
"并发是通过交错执行来处理多个任务的能力,而并行是在不同 CPU 核心上同时执行多个任务的能力。"
这句话基本正确,但不严谨。
- 前半句(并发 = 交错执行多个任务)✅ 准确
- 后半句(并行 = 在不同 CPU 核心上同时执行)❌ 过窄
并行确实包括"多核同时执行",但远不止于此。并行的实现形式有:
- 单机多核:多 CPU 核心
- SIMD 指令:单核内一条指令处理多个数据(向量并行)
- GPU 并行:数千个线程并行
- 多机分布式:多台机器并行(MPI、分布式计算)
- 指令级并行(ILP):单核内流水线、超标量乱序执行
并行对应的是"同时执行"这个结果,而不是某一种具体的硬件手段。
二、并发 vs 并行:一句话的本质区分
Rob Pike(Go 语言之父)有一句经典论断:
"Concurrency is about dealing with lots of things at once. Parallelism is about doing lots of things at once."
并发是"应对"很多事,并行是"执行"很多事。
翻译成工程师能秒懂的话:
| 维度 | 并发(Concurrency) | 并行(Parallelism) |
|---|---|---|
| 本质 | 结构(程序组织方式) | 执行(实际运行方式) |
| 关键词 | 应对、管理、交错 | 同时、真跑 |
| 单核可行 | ✅ 可以(时间片轮转) | ❌ 不可能真正同时 |
| 必要条件 | 多个独立推进的逻辑单元 | 多个硬件执行单元 |
并发是"一个工人同时接了三件活儿,在三件活儿之间来回切换着干"——交错。 并行是"三个工人各分到一件活,三人同时各干各的"——真正同时。
三、为什么并发是"交错执行"
回到一个物理事实:单核 CPU 同一时刻只能执行一条指令流。
那单核机器怎么"同时"做三件事?答案是 时间片轮转(time slicing)——CPU 被切成极小的时间片,在多个任务之间快速切换:
真实时间线(单核):
时刻1: 任务A ▌
时刻2: ▌任务B
时刻3: ▌任务C
时刻4: ▌任务A(继续)
时刻5: ▌任务B(继续)
...
给人的感觉:A、B、C 在"同时"跑
实际真相:它们被切碎穿插执行,切得极快(毫秒级),人感知不到
这就是"交错执行"(interleaving)的字面意思:把多个任务切成小片,交替着推进,制造出"同时"的幻觉。
一个具象例子
你在单核电脑上同时开着浏览器、音乐播放器、微信,它们都在"运行"。但 CPU 只有一颗核心,操作系统的做法是:
- 给浏览器跑 10ms
- 切走去给播放器跑 10ms(解一帧音频)
- 再切去给微信跑 10ms(收一次消息)
- 回到浏览器……
每秒切换几百次。三个任务类型不同,但因为被交错切片推进,看起来都在"持续进行"。这就是单核并发。
对比:并行不是"交错"
真实时间线(多核,3 核各跑一个任务):
核心1: 任务A A A A A A A A... (连续跑,不切换)
核心2: 任务B B B B B B B B... (连续跑,不切换)
核心3: 任务C C C C C C C C... (连续跑,不切换)
每个核心专注一摊,真正同时,不切来切去。
四、"多个任务"是什么:同类型还是不同类型?
这是新手最容易卡的点。答案:不区分类型。
并发语境下的"任务"指的是一个逻辑执行单元(一段独立的工作),跟它是同一种业务还是不同业务无关:
| 场景 | 任务类型 | 是否并发 |
|---|---|---|
| Web 服务器同时处理 100 个 HTTP 请求 | 同类型(都是处理请求) | ✅ 并发 |
| 服务器一边接请求、一边写日志、一边心跳上报 | 不同类型 | ✅ 并发 |
| 单线程循环累加一亿次 | 同一个 | ❌ 不是并发 |
| Word 一边打字、一边拼写检查、一边自动保存 | 不同类型 | ✅ 并发 |
关键:并发关心"有几个独立推进的执行单元在交替跑",不在乎它们干的是不是同一件事。
五、重头戏:单核怎么扛 100 个并发请求?
设想这个场景:
一个接口,100 个人同时请求过来,CPU 单核怎么处理?
直觉上,单核一次只能干一件事,怎么顶得住 100 个并发?
答案就是你脑海里已经形成的那个画面:
单核(真实情况):
时刻 t0: CPU 在跑请求#1 ▌ (其余 99 个等着)
时刻 t1: CPU 在跑请求#2 ▌
时刻 t2: CPU 在跑请求#3 ▌
...
时刻 t99: CPU 在跑请求#100 ▌
时刻 t100: 又回到请求#1 继续 ▌
...
任意瞬间:只有 1 个请求在真正执行
一秒内: 每个请求都被推进了十几万次,加起来像 100 个都在跑
✅ 你理解对了:单核分时段处理,第 1 个处理一部分就快速切走,再处理第 2 个,再切,再切……营造并行的幻觉,实际只有一个 CPU 在处理这 100 个请求。
六、但这里有个关键:CPU 真的"全程都在算"吗?
这是新手最容易忽略、也最该想明白的一点。
Web 请求处理绝大部分时间 CPU 是闲着的。一个 HTTP 请求的时间花在哪:
| 阶段 | 谁在干活 | CPU 占用 |
|---|---|---|
| 网络接收数据 | 网卡 + 内核 | 几乎不占 CPU |
| 解析请求、跑业务逻辑 | CPU | 真正占 CPU |
| 查数据库、等返回 | 网络 + 数据库进程 | CPU 干等 |
| 调下游接口、等响应 | 网络 + 下游 | CPU 干等 |
| 网络发送响应 | 网卡 + 内核 | 几乎不占 CPU |
一个典型接口,CPU 真正在算的时间可能只占 5%~20%,剩下的 80%+ 都在等——等数据库、等下游、等网络。
这个"等"恰恰是单核能扛高并发的秘密
正因为请求大部分时间在"等 I/O"而不是"占 CPU",单核并发才玩得转:
没有"等"的情况(纯计算,比如算素数):
请求#1: CPU算算算算算算算算...(死占 CPU 100ms)
请求#2: 必须等 #1 算完才能开始
→ 单核下 100 个请求要排队,慢死
有"等"的情况(典型 Web 请求):
请求#1: CPU 算 1ms → 查数据库(干等 9ms) → CPU 算 1ms 完成
请求#2: 趁 #1 在等数据库的 9ms 里,CPU 跑去算 #2 → #2 也去等数据库...
→ 单核下 100 个请求大部分时间能重叠推进,CPU 不闲着也不互相挡道
这就是为什么 I/O 密集型场景,单核异步并发就够了。CPU 既能服务 100 个请求,自己还不忙。Go 的 goroutine、Node.js 的事件循环、Python 的 async/await,干的就是这件事:
当一个请求去等 I/O 时,立刻把 CPU 让给下一个请求;等 I/O 回来了再切回来继续。
七、反过来:100 个请求都是 CPU 密集型呢?
比如都是"给我把这张图做高斯模糊"——纯 CPU 计算,没有 I/O 等待:
请求#1: CPU 算算算算算算...(占满内核 10ms,不喘气)
请求#2: 排队等 #1 算完
...
单核处理 100 个:约 1000ms 总耗时,每个请求平均等约 500ms
这时单核并发没救了——交错执行也省不出时间,因为每个任务都死咬着 CPU 不松口,切来切去反而有切换开销,100 个请求被串行成一条长队。
唯一办法就是上并行——多核,让多核各自真同时算几个:
4 核并行:
核心1: 请求 #1, #5, #9 ... (真同时各自算)
核心2: 请求 #2, #6, #10 ...
核心3: 请求 #3, #7, #11 ...
核心4: 请求 #4, #8, #12 ...
→ 总耗时降到约 250ms,快 4 倍
八、把一切收敛成一句话
- Web 接口场景:单核分时段切来切去服务 100 个请求,是单核并发的标准玩法;能跑得动,是因为请求大部分时间在等 I/O,CPU 趁等的时候去服务别人。
- CPU 密集型场景:同样的切法就玩不转了,CPU 一直被占着没空隙可切,必须靠多核并行才能真正加速。
所以"并发 = 交错执行"的完整真相是:
交错只有在任务会主动让出 CPU(等 I/O、yield、sleep)时才跑得顺;纯计算任务不让出,交错就退化成排队。
并发解决的是"如何管理多个任务"——让它们轮流占资源、不互相阻塞死。 并行解决的是"如何加速"——多个干活的人同时上。