同步、异步、阻塞、非阻塞、并发、并行
在刚接触系统编程和高并发架构时,我对并发和并行的理解非常简单:
“并发就是阻塞等待,并行就是多个程序同时执行。”
后来随着深入学习,我发现“并行”的理解方向基本正确,但把“并发”和“阻塞等待”划等号,其实是混淆了代码调用机制、线程状态与硬件调度执行这三个不同维度的概念。
如果用一句话来区分最常混淆的两组概念,那就是:
同步/异步看“结果怎么拿”,并发/并行看“任务怎么跑”。
一、三维解耦:它们到底在关注什么?
- 将这六个概念拆解到各自的关注维度,关系就会一目了然:
| 概念维度 | 包含概念 | 核心关注点 | 一句话说明 |
|---|---|---|---|
| 编程模型层 (API 设计) | 同步 / 异步 | 结果怎么拿? | 同步:主动等待结果返回。 异步:不等结果,好了通过回调/通知告诉我。 |
| 线程状态层 (执行状态) | 阻塞 / 非阻塞 | 等待时线程在干嘛? | 阻塞:没拿到结果前线程挂起死等。 非阻塞:没拿到结果前立刻返回,线程先干别的。 |
| 系统调度层 (硬件执行) | 并发 / 并行 | 多任务怎么跑? | 并发:时间段内交替推进多任务(单核即可)。 并行:时间点上物理同时执行多任务(依赖多核)。 |
二、 并发 vs 并行:时间轴上的物理差异
- 并发 (Concurrency):关注处理多件事的能力。利用时间片轮转或协程调度,在单核 CPU 上也能快速切换推进多个任务。
- 并行 (Parallelism):关注同时做多件事的能力。依靠物理多核 CPU,在同一时刻真正同步运行多个任务。
时间轴 ───────────────────────────────────────────────────────────►
【单核 CPU 并发 (Concurrency)】:交替推进
CPU Core 1: [ 任务 A ] [ 任务 B ] [ 任务 A ] [ 任务 C ] [ 任务 B ]
【多核 CPU 并行 (Parallelism)】:物理同时
CPU Core 1: [ 任务 A 正在物理执行... ]
CPU Core 2: [ 任务 B 正在物理执行... ]
CPU Core 3: [ 任务 C 正在物理执行... ]三、 破除误区:四种组合的代码实战
- “同步就是阻塞,异步就是非阻塞”是典型的概念混淆。它们属于不同维度,可以自由组合:
1. 同步 + 阻塞(最传统)
生活比喻:窗口排队打饭
你去食堂窗口打饭,必须等前面的菜做好递到你手上,你才能离开窗口去寻找座位。期间你只能站在那里死等,什么也干不了。
// 线程卡死在这行代码死等文件读取完成,期间啥也干不了
const data = fs.readFileSync("a.txt");
console.log(data); // 必须等读取完,才能执行下一行2. 同步 + 非阻塞(轮询)
生活比喻:不停去前台询问
你在餐馆点餐,店员说还没做好。你先回座位坐下,但因为你需要主动拿到菜(同步),所以你每隔 1 分钟就跑去前台问一次:“我的菜好了吗?”没好你就回座位继续刷手机,好了就端走。
// 发起调用立刻返回;虽然主动等结果(同步),但等待期间线程可以干别的
while (true) {
const data = tryRead();
if (data) { process(data); break; }
doOtherThings(); // 没好就先做其他事
sleep(100);
}3. 异步 + 阻塞(罕见,但技术上存在)
生活比喻:叫了外卖,却站在门口死等
你点了外卖(异步:做饭和送餐都交给了厨师和小哥,你不需要亲力亲为)。但点完之后,你啥也不干,直接搬个板凳站在门口死死盯着大门,一直等到外卖送达(阻塞)。
// 1. 异步:将耗时任务派发给后台线程池处理(当前线程本可以去干别的)
Future<Data> future = executor.submit(() -> fetchRemoteData());
// 2. 阻塞:当前线程主动调用 get(),挂起自己死等后台线程返回结果
Data data = future.get(); // 当前线程在此处卡死挂起!4. 异步 + 非阻塞(最高效,现代 Web 标配)
生活比喻:取号后坐下玩手机,叫号再去取
你在餐馆扫码点餐后拿到一个取餐呼叫器,你立刻找个座位坐下专心玩手机/处理工作(非阻塞)。等呼叫器响了(异步通知),你再去柜台取餐。
// 1. 发起异步网络请求(非阻塞:调用立刻返回,主线程不会在此挂起等待)
fetch("https://api.example.com/data")
.then((res) => res.json())
.then((data) => {
// 3. 很久之后网络数据返回时,才执行这个回调函数
console.log("【异步通知】网络数据加载完成:", data);
});
// 2. 主线程完全没有卡死,立刻往下执行其他高强度任务
console.log("【主线程】开始处理其他任务(如计算图表数据)...");
for (let i = 0; i < 3; i++) {
console.log(`【主线程】正在计算模块 ${i + 1}...`);
}
// 控制台输出顺序:
// 1. 【主线程】开始处理其他任务(如计算图表数据)...
// 2. 【主线程】正在计算模块 1...
// 3. 【主线程】正在计算模块 2...
// 4. 【主线程】正在计算模块 3...
// 5. 【异步通知】网络数据加载完成: { ... } <-- 最后才打印四、 全景图与架构选型映射
- 不同的场景与技术组件,对应的模型组合完全不同:
┌─────────────────────────────────────────────────────────────────┐
│ 一个网络请求的处理流程 │
└─────────────────────────────────────────────────────────────────┘
用户发起请求
│
▼
┌─────────────┐
│ 同步/异步? │ ← 编程模型层:这个 API 是怎么设计的?
│ (结果怎么来) │ 同步 → 调用后一直等到结果
└─────────────┘ 异步 → 调用后立即返回,结果通过回调通知
│
▼
┌─────────────┐
│ 阻塞/非阻塞? │ ← 线程状态层:等待结果的时候线程在干嘛?
│ (等的时候干嘛)│ 阻塞 → 线程被挂起,不能做任何事
└─────────────┘ 非阻塞 → 线程立即返回,可以干别的
│
▼
┌─────────────┐
│ 并发/并行? │ ← 系统调度层:多个请求之间怎么执行?
│ (多任务怎么跑)│ 并发 → 单核交替处理,利用等待时间切换
└─────────────┘ 并行 → 多核真正同时处理五、核心总结与决策法则
- 在实际架构设计中,面对不同的任务瓶颈,技术选型的方向截然不同:
┌───────────────────────────────┐
│ 高并发系统架构 │
└───────────────┬───────────────┘
│
┌──────────────────┴──────────────────┐
▼ ▼
【I/O 密集型任务】 【CPU 密集型任务】
(网络请求、数据库查询、文件读写) (数据加密、图像渲染、科学计算)
│ │
▼ ▼
异步 + 非阻塞 + 并发 多核 + 多线程/多进程 + 并行
(如 Node.js / Nginx / Go 协程) (如 Worker Threads / OpenMP)重要
I/O 密集型:瓶颈在于等待外部设备响应。采用 异步 + 非阻塞 + 并发,避免线程卡死在死等上,以极低的系统消耗换取极高吞吐量。
CPU 密集型:瓶颈在于纯计算。单核上的频繁切换毫无意义,必须依靠 多核 + 多线程/多进程 + 并行 才能成倍缩短计算耗时。
总结
- 并发就是阻塞等待 → 并发是利用等待时间交替推进多个任务。
- 并行就是多个程序同时执行 → 完全准确,需要物理多核/分布式节点支撑。
- 同步/异步与阻塞/非阻塞 → 分别解答了应用层结果通知和底层线程状态的问题。
版权所有
版权归属:念宇
