同步、异步、阻塞、非阻塞——这四个词到底在说什么?
说实话,我见过不少刚入行的朋友,一听到这组概念就有点晕。
同步、异步、阻塞、非阻塞……看起来像四个不同的词,其实它们就是两个维度交叉出来的四种组合。搞懂了这一点,后面那些回调、事件循环、协程什么的,都会顺很多。
我自己当年也是绕了点弯路才理清楚的。这次我把它们掰开揉碎了讲,保证你看完能一口说清楚。
先从两个维度说起
你要理解这四个词,得先明白它们是从哪两个角度去描述一个调用的。
- 第一个维度:调用方要不要等结果? —— 这是「同步 vs 异步」.
- 第二个维度:被调用方在等待时能不能干别的事? —— 这是「阻塞 vs 非阻塞」.
嗯,注意了,这里很多人会搞混。同步不等于阻塞,异步也不等于非阻塞。这两组概念描述的对象不一样。
核心口诀:
- 同步/异步 —— 说的是调用方是否等待结果.
- 阻塞/非阻塞 —— 说的是调用方在等待期间线程的状态.
用大白话翻译一下:
- 同步:你打电话订外卖,一直举着电话等对方说「好了」才挂断.
- 异步:你发了个微信订外卖,然后该干嘛干嘛,等外卖到了再通知你.
- 阻塞:你等快递的时候,什么事都不做,就盯着门口发呆.
- 非阻塞:你等快递的同时还在刷剧、做饭、遛狗,偶尔瞄一眼门口.
现在你再看,同步和阻塞其实不是一回事。同步调用的时候,线程可以阻塞也可以不阻塞,取决于你具体怎么实现。
四象限矩阵
把这两个维度组合一下,就得到了经典的四种调用模式。
| 同步 | 异步 | |
|---|---|---|
| 阻塞 | 同步阻塞 | 异步阻塞 |
| 非阻塞 | 同步非阻塞 | 异步非阻塞 |
我来一个个说清楚。
同步阻塞 —— 最朴素,也最直白
这是大多数新手写的代码风格。你调用一个函数,函数没返回之前,你就在那干等着,直到拿到结果才能往下走。
int result = read_data_from_socket(); // 这一行不返回,就不往下走
printf("%d", result);
我在项目中遇到过不少这样的场景,比如读取数据库、写日志、文件IO,默认都是同步阻塞模型。优点是逻辑简单,缺点是线程利用率低——线程在等IO时,100% 在空转。
同步非阻塞 —— 轮询,累但灵活
调用方还是会主动等待结果,但线程在等待期间不会死等。它会不断地去查问「好了没?好了没?」。
while (true) {
int result = check_if_data_ready(); // 非阻塞调用
if (result == READY) {
process_data();
break;
}
// 做点别的事,或者短暂睡眠
sleep(10);
}
这种模式在低并发场景下能用,但如果轮询频率太高,CPU 会被白白吃掉。我个人习惯很少用裸轮询,除非是嵌入式或者底层驱动场景。
异步阻塞 —— 看似矛盾,实际存在
很多人觉得「异步」和「阻塞」不可能同时出现。其实不然。异步阻塞的意思是:调用方发起异步请求后,并不立即返回,而是阻塞在一个地方等待异步完成。举个例子:你用 select() 系统调用,它会阻塞等待一组 fd 中有任何一个就绪,这就是典型的异步阻塞。
int ret = select(max_fd, &readfds, NULL, NULL, NULL); // 阻塞等待
// 一旦返回,说明有数据可读
嗯,这里要注意,异步阻塞在 IO 多路复用里很常见。你想想看,如果不用阻塞,你就得一直轮询,那就回到同步非阻塞的老路上了。
异步非阻塞 —— 最优雅,也最难写
这是高性能系统的追求。调用方发起调用后,立刻返回,不阻塞;等到真正有结果时,通过回调、事件或者协程来通知。
// 伪代码示意
async_read_data(callback: (err, data) -> {
if (err) handle_error(err);
process_data(data);
});
// 这一行立即执行,不会等数据读完
do_other_stuff();
我建议做高并发 API 网关或者 RPC 框架的同学,重点理解这个模式。它的核心思想就是「不要把线程浪费在等待上」。我曾经在一个网关项目里把同步阻塞改成异步非阻塞,同样 4 核 8 G 的机器,并发能力从 500 提到了接近 5000。
一个小技巧:初学者容易混淆「异步」和「并发」,它们真的不是一回事。异步是一种调用风格,并发是一种执行方式。你可以写异步但单线程的代码(比如 Node.js),也可以写同步但多并发的代码(比如 Java 多线程)。
一张图把四者关系画清楚
避坑指南:我曾经以为异步就一定是非阻塞的,于是写了一个异步调用但内部用 CountDownLatch 阻塞等待返回的代码。结果锁住了整个线程池。记住——异步 + 阻塞的组合,很多人会不小心引入,而且很难排查。如果你用了异步框架,就别在回调链里加锁或阻塞原语。
实际场景怎么选这张表就够了
| 场景 | 推荐模式 | 原因 |
|---|---|---|
| 文件读写 | 同步阻塞 | 操作系统底层就是这模型,别折腾 |
| 高并发网络 IO | 异步非阻塞 | 线程不能浪费在等待上 |
| 简单 CRUD API | 同步阻塞或同步非阻塞 | 并发不高时,代码可维护性更重要 |
| 消息队列消费 | 异步阻塞(拉模式) | 用 pull 长轮询一样高效 |
| GUI / UI 事件 | 异步非阻塞 | 不能卡住用户界面 |
说白了,这四种模式没有的好坏。你想想看,你说它不复杂,但聊深了也挺有意思;你说它复杂,但本质上就是两个维度的事。
搞清楚了这组基础概念,后面再聊回调地狱、Promise、async/await、协程,你就会发现都是在同一个坐标系里找更好的写法。嗯,先把这四个词刻在脑子里,后面就好办了。