同步、异步、阻塞、非阻塞——这四个词到底在说什么?

说实话,我见过不少刚入行的朋友,一听到这组概念就有点晕。

同步、异步、阻塞、非阻塞……看起来像四个不同的词,其实它们就是两个维度交叉出来的四种组合。搞懂了这一点,后面那些回调、事件循环、协程什么的,都会顺很多。

我自己当年也是绕了点弯路才理清楚的。这次我把它们掰开揉碎了讲,保证你看完能一口说清楚。

先从两个维度说起

你要理解这四个词,得先明白它们是从哪两个角度去描述一个调用的。

  • 第一个维度:调用方要不要等结果? —— 这是「同步 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 多线程)。

一张图把四者关系画清楚

四种调用模式关系图 维度一:调用方是否等待(同步/异步) 维度二:等待时线程是否挂起(阻塞/非阻塞) 同步阻塞 同步非阻塞 异步阻塞 异步非阻塞 等待 + 挂起 最常用,逻辑简单 等待 + 轮询 CPU 空转风险 等通知 + 挂起 IO 多路复用 不等待 + 回调 最高效,最难写 如何选择? 低并发:选同步阻塞 多连接:选异步阻塞 高性能:选异步非阻塞 极致实时:同步非阻塞

避坑指南:我曾经以为异步就一定是非阻塞的,于是写了一个异步调用但内部用 CountDownLatch 阻塞等待返回的代码。结果锁住了整个线程池。记住——异步 + 阻塞的组合,很多人会不小心引入,而且很难排查。如果你用了异步框架,就别在回调链里加锁或阻塞原语。

实际场景怎么选这张表就够了

场景 推荐模式 原因
文件读写 同步阻塞 操作系统底层就是这模型,别折腾
高并发网络 IO 异步非阻塞 线程不能浪费在等待上
简单 CRUD API 同步阻塞或同步非阻塞 并发不高时,代码可维护性更重要
消息队列消费 异步阻塞(拉模式) 用 pull 长轮询一样高效
GUI / UI 事件 异步非阻塞 不能卡住用户界面

说白了,这四种模式没有的好坏。你想想看,你说它不复杂,但聊深了也挺有意思;你说它复杂,但本质上就是两个维度的事。

搞清楚了这组基础概念,后面再聊回调地狱、Promise、async/await、协程,你就会发现都是在同一个坐标系里找更好的写法。嗯,先把这四个词刻在脑子里,后面就好办了。