You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C语言实现非阻塞异步文件读取:pread/aio_read及线程池疑问

在C语言中实现无阻塞/异步文件读取的方案

一、为什么O_NONBLOCK对pread(2)无效?

POSIX标准里,O_NONBLOCK标志仅针对套接字、管道这类流类型文件设计,用于避免数据未就绪时的阻塞。但对于本地磁盘上的常规文件,内核会直接忽略这个标志——因为本地文件的"数据就绪"逻辑是只要文件存在、偏移合法,数据理论上是可获取的,只是需要从磁盘加载到内存页缓存。此时pread/read的阻塞本质是磁盘I/O的耗时,和O_NONBLOCK无关。你测试中读取1GB数据阻塞300ms,就是磁盘实际的读写时间。

二、实现无等待+轮询的文件读取

针对你的需求(无等待提交请求、轮询检查完成状态),分平台给出可行方案:

1. Linux平台:io_uring(推荐)

io_uring是Linux 5.1+引入的高性能异步IO框架,完美匹配你的场景:单线程即可批量处理异步IO,支持轮询式检查完成状态,性能远优于aio_read。

核心代码示例:

#include <liburing.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

#define QUEUE_DEPTH 1024
#define BUF_SIZE 4096

void do_other_work() {
    // 轮询间隙处理其他业务逻辑
}

int main() {
    struct io_uring ring;
    struct io_uring_params params = {
        .flags = IORING_SETUP_IOPOLL // 开启主动轮询模式,避免内核通知开销
    };
    io_uring_queue_init_params(QUEUE_DEPTH, &ring, &params);

    // O_DIRECT跳过页缓存,适合高负载随机读取场景
    int fd = open("target_file", O_RDONLY | O_DIRECT);
    if (fd < 0) {
        perror("open failed");
        return 1;
    }

    char buf[BUF_SIZE];
    struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
    // 提交异步pread请求(偏移0,读取4KB)
    io_uring_pre_pread(sqe, fd, buf, BUF_SIZE, 0);
    io_uring_submit(&ring);

    // 轮询检查请求完成状态
    struct io_uring_cqe *cqe;
    while (1) {
        int ret = io_uring_peek_cqe(&ring, &cqe);
        if (ret == 0) {
            // 请求完成,处理结果
            if (cqe->res >= 0) {
                printf("读取成功,字节数:%d\n", cqe->res);
            } else {
                printf("读取失败,错误码:%d\n", -cqe->res);
            }
            io_uring_cqe_seen(&ring, cqe);
            break;
        }
        do_other_work();
    }

    io_uring_queue_exit(&ring);
    close(fd);
    return 0;
}
  • IORING_SETUP_IOPOLL:让应用主动轮询完成队列,消除内核通知的额外开销
  • O_DIRECT:跳过内核页缓存,高负载下能充分发挥NVMe的随机读取性能;若为顺序读,可保留页缓存提升效率

2. macOS(M1)平台:kqueue + AIO

macOS不支持io_uring,可通过kqueue监听异步IO完成事件,实现轮询式检查:

核心代码示例:

#include <aio.h>
#include <sys/event.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

#define BUF_SIZE 4096

void do_other_work() {
    // 轮询间隙处理其他业务逻辑
}

int main() {
    int kq = kqueue();
    if (kq < 0) {
        perror("kqueue create failed");
        return 1;
    }

    int fd = open("target_file", O_RDONLY);
    if (fd < 0) {
        perror("open failed");
        return 1;
    }

    char buf[BUF_SIZE];
    struct aiocb aiocb = {
        .aio_fildes = fd,
        .aio_buf = buf,
        .aio_nbytes = BUF_SIZE,
        .aio_offset = 0,
        .aio_sigevent.sigev_notify = SIGEV_NONE // 禁用信号通知,用kqueue监听
    };

    // 提交异步读请求
    if (aio_read(&aiocb) < 0) {
        perror("aio_read failed");
        return 1;
    }

    // 注册kqueue事件监听AIO完成
    struct kevent event;
    EV_SET(&event, (uintptr_t)&aiocb, EVFILT_AIO, EV_ADD, 0, 0, NULL);
    kevent(kq, &event, 1, NULL, 0, NULL);

    // 轮询检查完成状态
    struct kevent result;
    while (1) {
        int ret = kevent(kq, NULL, 0, &result, 1, NULL);
        if (ret > 0) {
            ssize_t bytes_read = aio_return(&aiocb);
            if (bytes_read >= 0) {
                printf("读取成功,字节数:%zd\n", bytes_read);
            } else {
                printf("读取失败,错误码:%d\n", errno);
            }
            break;
        }
        do_other_work();
    }

    close(kq);
    close(fd);
    return 0;
}

三、线程池方案的线程切换开销解析

如果因平台限制必须使用线程池,需明确以下开销细节:

  1. 直接切换开销:现代CPU(如M1)上,单次线程上下文切换(保存/恢复寄存器、页表等)开销约1-5微秒。若线程池大小合理(等于CPU核心数),线程绑定核心运行,切换次数会极少。
  2. 优化关键点:
    • 线程数量:不要超过CPU核心数(CPU+IO混合负载可设为核心数*2),否则会引发频繁切换
    • 任务队列:使用无锁队列(如moodycamel::ConcurrentQueue)减少锁竞争
    • CPU绑定:通过pthread_setaffinity_np(Linux)或thread_policy_set(macOS)将线程绑定到固定核心,提升缓存命中率
  3. 适用场景:若已有成熟线程池架构,或需兼容老平台,线程池是可行方案;但对于高负载小批量读取(1KB-4KB),异步IO(io_uring/kqueue)性能远高于线程池——因为单线程可处理数千并发IO请求,无需线程挂起与切换。

四、针对高负载NVMe场景的额外建议

  • 用O_DIRECT跳过页缓存,避免高负载下内核缓存的竞争瓶颈
  • 批量提交请求:一次提交多个读取请求(如100个4KB请求),填满NVMe的IO队列,提升磁盘利用率
  • 尽可能排序读取请求:按磁盘块顺序发起读取,利用NVMe的顺序读取优势

内容的提问来源于stack exchange,提问作者BlobKat

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 19:03:26