异步(Asynchronous) 和 非阻塞(Non-blocking)

一、阻塞 vs 非阻塞(Blocking vs Non-blocking)

核心区别: 发起 I/O 调用时,线程是否会被挂起等待结果。

  1. 阻塞 I/O (Blocking I/O)

    • 含义:当线程发起 I/O 操作(如 read() 读取网络数据),若数据未就绪,线程会被操作系统挂起(睡眠),直到数据准备好并被内核拷贝到用户空间后,线程才被唤醒继续执行。

    • 比喻:你去银行柜台办业务,柜员说“需要后台处理,你在这等着”。你只能干等(线程阻塞),直到柜员处理完把结果给你。

    • 代码表现:read() 函数不返回,线程卡在这一行。

    • 问题:高并发时,每个连接需一个线程,资源消耗大。

  2. 非阻塞 I/O (Non-blocking I/O)

    • 含义:当线程发起 I/O 操作,若数据未就绪,函数立即返回一个错误(如 EAGAIN),线程不会被挂起,可以继续执行其他任务。

    • 如何实现:通过 fcntl(fd, F_SETFL, O_NONBLOCK) 将文件描述符设为非阻塞模式。

    • 比喻:柜员说“后台在处理,你可以先去干别的,但需要你自己时不时回来问是否完成”。

    • 代码表现:调用 read() 可能返回 -1 且 errno == EAGAIN,线程继续执行后续逻辑。

    • 优点:线程不被阻塞,可同时处理多个 I/O。

    • 缺点:需主动轮询检查状态,浪费 CPU。


二、同步 vs 异步(Synchronous vs Asynchronous)

核心区别: I/O 操作的完成结果如何通知调用者。

  1. 同步 I/O (Synchronous I/O)

    • 含义:线程主动等待并获取结果。包括两类:

      • 阻塞型同步:线程挂起等待操作完成(如默认的 read())。

      • 非阻塞型同步:线程轮询检查状态(如非阻塞 read() + 循环重试)。

    • 本质:I/O 操作的完成仍需线程主动参与(等待或轮询)。

    • 比喻:无论是干等(阻塞)还是反复询问(非阻塞),最终都是你自己拿到结果。

    • 技术代表:read()write()select()poll()epoll_wait() + 非阻塞 read()

  2. 异步 I/O (Asynchronous I/O, AIO)

    • 含义:线程发起 I/O 操作后立即返回,内核完成整个操作(包括数据拷贝)后,通过回调、信号或事件通知线程结果。

    • 本质:内核完全接管 I/O 操作,线程无需关心过程。

    • 比喻:柜员说“留下地址,办完我快递给你”。你直接回家,收到包裹即完成。

    • 代码表现:

      // Linux 原生 AIO 示例
      struct iocb cb = {0};
      cb.aio_fildes = fd;          // 文件描述符
      cb.aio_buf = buffer;         // 用户缓冲区
      cb.aio_nbytes = size;        // 数据大小
      cb.aio_lio_opcode = IOCB_CMD_PREAD; // 读操作
      io_submit(ctx, 1, &cb);      // 提交请求(立即返回)
      // 内核完成后通过回调或 io_getevents() 通知
    • 优点:线程完全自由,无轮询开销。

    • 缺点:编程复杂,操作系统支持不完善(如 Linux 原生 AIO 对网络 I/O 支持有限)。


三、关键区别总结

特性非阻塞 I/O异步 I/O
关注点 调用是否立即返回 结果如何通知
调用行为 立即返回(成功/错误) 立即返回(仅表示提交成功)
数据就绪时 需线程主动调用 read/write 内核自动拷贝数据到用户缓冲区
完成通知 无通知,需线程轮询或事件触发 回调、信号、事件通知
线程参与 需线程执行实际 I/O 操作 线程完全不参与 I/O 操作过程
典型代表 read(fd, buf, size) + O_NONBLOCK Linux io_uring / Windows IOCP

🔥 重要认知:
epoll 本质是同步非阻塞模型!

  • epoll_wait() 返回时,仅告诉你哪些 fd 可进行不阻塞的 I/O 操作(数据在内核缓冲区就绪)。

  • 你仍需调用非阻塞的 read()/write() 将数据从内核空间拷贝到用户空间(这个拷贝过程是同步的)。


四、“异步非阻塞”组合的含义

当同时使用两种技术时:

  1. 非阻塞:发起 I/O 调用不阻塞线程(立即返回)。

  2. 异步:内核完成整个 I/O 后主动通知结果(无需线程轮询)。

典型场景:

  • 现代高性能框架(如 Java NIO + AIO、Node.js libuv)通过 事件循环 + 线程池 模拟异步:

    • 主线程用 epoll 管理 I/O 事件(同步非阻塞)。

    • 将耗时操作(文件 I/O、数据库)提交给线程池,主线程通过回调获知结果(异步)。


五、一张图理解四种 I/O 模型

https://img-blog.csdnimg.cn/20210531175809673.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkzNDYwNw==,size_16,color_FFFFFF,t_70#pic_center

  1. 阻塞 I/O:全程等待。

  2. 非阻塞 I/O:轮询直到数据就绪。

  3. I/O 多路复用(epoll):批量监控 fd,就绪后同步拷贝数据。

  4. 异步 I/O:提交请求后完全不管,内核完成所有步骤后通知。


总结

  • 非阻塞:解决 “调用是否卡住线程” 问题(立即返回)。

  • 异步:解决 “操作完成后如何告知” 问题(内核主动通知)。

  • epoll 是同步非阻塞模型:它高效管理 fd 状态,但数据拷贝仍需用户线程参与。

  • 真正的异步 I/O:内核全程负责 I/O 操作(包括数据拷贝),用户线程彻底解放。

实际开发中,常将 非阻塞 I/O + I/O 多路复用(如 epoll) + 事件循环 组合成高并发架构,在性能和复杂度间取得平衡。而真正的异步 I/O(如 io_uring)是未来发展方向。

posted @ 2025-06-22 06:28  郭慕荣  阅读(132)  评论(0)    收藏  举报