Linux下用io_uring可实现真正的异步文件读取,需O_DIRECT打开文件、4KB对齐缓冲区;Windows用ReadFile+OVERLAPPED模拟异步,须FILE_FLAG_OVERLAPPED打开句柄;std::async等线程方案本质仍同步,无法复用内核I/O队列。

Linux下用io_uring做真正的异步文件读取
标准C++没有内置AIO支持,std::ifstream和fread全是阻塞的。想不卡线程地读文件,得靠操作系统提供的异步I/O机制。Linux 5.1+最靠谱的选择是io_uring——它比传统的libaio更轻量、支持更多操作、且无需额外线程轮询。
关键点:必须用O_DIRECT打开文件(绕过页缓存),且缓冲区地址需按4KB对齐;否则io_uring会直接返回-EINVAL错误。
- 用
posix_memalign分配对齐内存,别用new或malloc - 文件打开时加
O_DIRECT | O_RDONLY标志,普通O_RDONLY会失败 - 提交
IORING_OP_READ前要调用io_uring_sqe_set_data绑定用户数据,方便完成时识别上下文 - 一次提交后调用
io_uring_submit触发,再用io_uring_wait_cqe等结果——这一步不阻塞整个进程,但当前线程会挂起,适合配合线程池用
Windows上用ReadFile + OVERLAPPED模拟AIO
Windows没有“真异步”磁盘I/O(NTFS底层仍可能同步等待),但ReadFile配合OVERLAPPED结构能实现非阻塞语义:调用立即返回,后续通过GetQueuedCompletionStatus或事件对象获知完成。
常见错误是忘了设置hEvent字段或没用FILE_FLAG_OVERLAPPED打开文件句柄——这时ReadFile直接返回FALSE,GetLastError()是ERROR_IO_PENDING才表示真正异步启动了。
立即学习“C++免费学习笔记(深入)”;
- 文件必须用
CreateFile以FILE_FLAG_OVERLAPPED打开,否则OVERLAPPED无效 -
OVERLAPPED结构体的hEvent可设为NULL,但之后必须用I/O完成端口(IOCP)或GetQueuedCompletionStatus收结果 - 缓冲区不用特殊对齐,但不能是栈变量(异步期间栈可能已销毁),建议用
std::vector<char></char>堆分配并保持生命周期 - 不要在回调里直接处理业务逻辑,尤其涉及GUI或STL容器——优先投递到主线程队列
为什么不用std::async或线程池“假装”异步
用std::async(std::launch::async, []{ /* read file */ })只是把阻塞挪到另一个线程,本质仍是同步读取+线程切换开销。当并发读取大量小文件时,线程数暴涨、上下文切换成本压倒I/O本身,反而比单线程顺序读还慢。
真正异步的价值在于:单个线程管理成千上万个I/O请求,靠内核通知驱动流程。比如一个网络服务既要读配置文件、又要读用户上传、还要查本地缓存,全塞进一个io_uring实例里提交,完成时统一回调分发,线程数可以压到2–3个。
-
std::async无法复用内核I/O队列,每次都要新建线程或从池里抢资源 - 线程池方案无法感知磁盘忙闲,容易在SSD写放大或HDD寻道时堆积任务
- 跨平台封装成本高:Linux用
io_uring,Windows用IOCP,macOS只能靠kqueue+线程模拟,没法一套代码跑三端
兼容性与调试陷阱
io_uring在glibc 2.33+才有原生封装,旧系统得直接syscall;Windows IOCP要求目标机器至少WinXP SP3,但VS2015以上默认支持。调试时最常踩的坑是忘记检查返回值——io_uring_submit成功只代表入队,实际执行失败得看CQE里的res字段,负数才是错误码(比如-EAGAIN表示重试,-EBADF才是句柄问题)。
- Linux下用
strace -e trace=io_uring_enter,io_uring_setup确认系统调用是否被调用 - Windows下用
Process Monitor过滤ReadFile操作,看是否标记为Async - 缓冲区大小不是越大越好:超过64KB可能触发
io_uring内部切片,增加CQE数量;小于4KB又浪费对齐开销 - 别在
io_uring回调里调用printf或std::cout——它们不是异步安全的,可能死锁
真正难的不是发起异步读,而是把文件路径、偏移、缓冲区生命周期、错误重试、超时控制这些串成可维护的状态机。多数项目卡在这里,而不是API调用本身。


















