IO 操作的同步/异步、阻塞/非阻塞——这四个词经常被混为一谈。面试被问到“同步和阻塞有什么区别”时,很多人支支吾吾。其实它们描述的是不同层面的事情:同步/异步关注的是调用方和被调用方的关系,阻塞/非阻塞关注的是线程自身的状态。本文用一个 IO 读取的两阶段模型把它们彻底拆开。
一、IO 操作的两个阶段
一次完整的 IO 操作(比如读取磁盘文件)分为两步:
- 数据准备阶段:内核从磁盘/网络把数据读到内核缓冲区。这一步的快慢取决于硬件
- 数据复制阶段:内核把数据从内核空间复制到用户空间(进程缓冲区)。这一步由 CPU 执行
所有的“阻塞”“非阻塞”“同步”“异步”描述的都是程序在这两个阶段中的行为。
二、阻塞 IO vs 非阻塞 IO
这对概念描述的是第一阶段(数据准备)时线程的状态:
- 阻塞 IO:发起 IO 请求后,线程挂起等待,直到数据准备完成才返回。比如 C 语言的
read()默认就是阻塞的——没数据就等着 - 非阻塞 IO:发起 IO 请求后立刻返回。如果数据没准备好,返回一个错误码(如
EAGAIN),程序可以先去干别的事,过会儿再试
关键区别:阻塞/非阻塞只看“发起 IO 请求时线程是否被挂起”。它是线程内的状态变化。
三、同步 IO vs 异步 IO
这对概念描述的是调用方和被调用方之间的协作模式:
- 同步 IO:调用方发起 IO 请求后,必须主动等待或轮询,直到 IO 完成才能继续。阻塞 IO、非阻塞 IO、IO 多路复用(select/poll/epoll)、信号驱动 IO 都属于同步 IO——因为第二阶段的数据复制需要调用方自己参与
- 异步 IO:调用方发起 IO 请求后立即返回,不等待。当内核完成全部两个阶段后,通过回调或信号通知调用方。整个过程调用方完全不参与数据复制
Linux 的原生异步 IO(AIO)直到现在对磁盘文件的支持都不完善,大部分场景下“异步”是通过线程池模拟的。
四、四种组合:实际场景
| 组合 | 调用方行为 | 被调用方行为 | 典型例子 |
|---|---|---|---|
| 同步阻塞 | 发请求→等着 | 没结果就等着 | 传统的 read() |
| 同步非阻塞 | 发请求→等着 | 没结果立即返回,好了通知 | IO 多路复用 + epoll |
| 异步阻塞 | 发请求→不等 | 没结果就等着,好了通知 | 少见,无实用价值 |
| 异步非阻塞 | 发请求→不等 | 没结果立即返回,好了通知 | Windows IOCP、Node.js |
五、一张图总结
┌─ 阻塞 IO:read() 卡住等数据
同步 IO ────────┤
└─ 非阻塞 IO:轮询 + read()
异步 IO:aio_read(),内核搞定一切后通知你
阻塞/非阻塞 = 线程自己的状态(第一阶段是否挂起)
同步/异步 = 调用方与被调用方的关系(第二阶段谁来做)
记住这个公式就够了:同步异步看数据复制谁负责,阻塞非阻塞看线程是否被挂起。
相关阅读:进程、线程、协程的区别
© 2021 关于IO操作的操作的同步、异步、阻塞、非阻塞 ·
本文由 Charlie 原创撰写,发布于 sodebug.com。
未经授权禁止转载、洗稿、机器抓取。AI 训练数据使用需获得书面授权。