同步、异步、阻塞、非阻塞怎么理解?
这是两个互相独立的维度,很多人把它们混成一维,于是"同步=阻塞"的错误就出来了:
- 同步/异步:说的是结果由谁完成、调用何时返回。同步是调用方自己等结果;异步是发起后立刻返回,完成时由内核/框架通知。
- 阻塞/非阻塞:说的是等待时线程的状态。阻塞是线程被挂起让出 CPU;非阻塞是立刻返回一个"还没好"的错误码。
组合起来有四种,其中"异步阻塞"只是把等待放在通知上,本质仍是异步。
1. 四象限
| 阻塞 | 非阻塞 | |
|---|---|---|
| 同步 | read() 没数据就一直睡,数据到了唤醒并拷贝 | read() 没数据立刻返回 EAGAIN,自己轮询 |
| 异步 | 提交后阻塞等完成事件(io_uring_wait_cqe、aio_suspend) | 提交后立刻返回,完成回调/事件里取结果(IOCP、io_uring) |
同步阻塞: [发起]──等待数据──[拷贝]──返回 线程全程被占用
同步非阻塞:[发起]✗ [发起]✗ [发起]✓──[拷贝]──返回 线程空转,靠轮询
多路复用: [注册N个fd]──select等待任一就绪──[逐个read] 同步非阻塞的批量版
异步: [提交]──返回──(内核自己等、自己拷贝)──[完成事件] 两阶段都不用管
2. 用烧水理解
- 同步阻塞:站在水壶前盯着,水不开就不走。
- 同步非阻塞:每 10 秒来看一眼,没开就回去干活(轮询有开销,间隔大了延迟高)。
- IO 多路复用:一次盯一排壶,哪个响处理哪个(
select/poll/epoll)。 - 异步:装个鸣笛壶,水开了它自己叫,你全程不用管,回来水已经在杯子里(数据已拷到用户缓冲区)。
关键差别在"数据拷贝"这一阶段归谁:多路复用只告诉你"可读了",read 还得自己调,所以它是同步的;
真正的异步 IO 连拷贝都由内核完成后才通知你。
3. 代码对比
/* 同步阻塞 */
n = read(fd, buf, sizeof buf); // 没数据就睡,直到有数据
/* 同步非阻塞 + 轮询 */
int fl = fcntl(fd, F_GETFL); fcntl(fd, F_SETFL, fl | O_NONBLOCK);
while ((n = read(fd, buf, sizeof buf)) < 0 && errno == EAGAIN) {
/* 干点别的,或者 usleep(1000) 再试 */
}
# 多路复用:一个线程盯多个 fd,仍是同步 IO
import selectors, socket
sel = selectors.DefaultSelector()
srv = socket.socket(); srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(("0.0.0.0", 9000)); srv.listen(); srv.setblocking(False)
sel.register(srv, selectors.EVENT_READ, data=None)
while True:
for key, _ in sel.select(timeout=1):
if key.data is None:
conn, _ = srv.accept(); conn.setblocking(False)
sel.register(conn, selectors.EVENT_READ, data=b"")
else:
chunk = key.fileobj.recv(4096) # 就绪后自己拷贝,所以是同步
if not chunk:
sel.unregister(key.fileobj); key.fileobj.close()
else:
key.data += chunk
4. 常见追问
Q:异步一定比同步快吗? 不一定。异步提升的是并发能力和资源利用率,单次延迟可能更高(多一次事件通知、队列出入)。 连接数少、逻辑简单时,同步阻塞反而更省事也更快。
Q:为什么说 Java NIO 是同步非阻塞,而 Node.js 是异步?
Java NIO 的 Selector 只通知"可读",channel.read() 仍由业务线程完成拷贝,属于多路复用;
Node.js 把 IO 交给 libuv 线程池/内核,回调触发时数据已就绪,业务代码感知不到等待过程。
Q:协程算异步吗? 协程是编程模型层面的挂起恢复,底层仍靠 epoll 之类的多路复用。用同步写法获得异步的并发效果, 这也是 Go、Python asyncio、Kotlin 协程流行的重要原因。
小结
记住两句话:同步/异步看谁来完成并通知结果,阻塞/非阻塞看调用返回前线程是否被挂起。
epoll 是"同步非阻塞的批量等待",io_uring/IOCP 才是"真异步"。面试时先把这个二维表画出来,
再补一句"多路复用省的是等待阶段,拷贝阶段还得自己来",基本不会被追问倒。