• 基础 | NIO - [IO 发展]


    § 1 按时间顺序

    BIO

    • 同步阻塞 IO
      • 读写请求会阻塞至有数据可供读写
      • 等待内核数据就绪并将数据传输到阻塞区
    • 会为每个请求开辟一个线程
      因此遇到高并发时,线程数量会爆炸

    PIO

    • 同步伪非阻塞 IO
    • 通常配合线程池模型使用的 IO
    • 解决了线程的频繁创灭问题
      但最大线程数和等候队列都打满时依然爆炸

    NIO

    • 同步阻塞 IO
    • 读写会阻塞,但读写请求不会
    • 通过 Reactor 模式,注册 IO 事件
    • 可供读写的数据准备就绪时,才开始正式读写
    • 解决了从接收到读写请求,到可以真实进行读写之间的阻塞浪费

    AIO

    • 异步非阻塞 IO
    • 读写不会阻塞
    • 引入异步通道
    • 代码简单
    • 依赖 Netty

    § 2 同步 & 异步、阻塞 & 非阻塞

    同步异步
    阻塞BIO-
    非阻塞NIO / PIOAIO

    同步 & 异步

    • 针对系统进程,是操作系统底层读写层面上是否阻塞
    • 系统进程在等待数据就绪的时间范围内,是否阻塞
    • 现象上主要影响系统进程通知用户应用数据已经就绪的方式,阻塞 VS 回调

    阻塞 & 非阻塞

    • 针对用户应用,是 API 调用时获取返回的过程是否阻塞
    • 调用方发起一个请求后,接收到这个请求的响应之前,是否阻塞
    • 现象上主要影响 API 调用返回的即时性,立即 VS 稍后

    NIO 到底是异步阻塞还是同步非阻塞
    本帖更倾向于 同步非阻塞,虽然作者也更习惯于反着叫(符合项目开发经验),但还是尊重官方 API 的命名

    • 这个问题本质上是个文字游戏,类似 @Overload@OverWrite 是重载、重写还是重写、复写
    • 同步异步和阻塞非阻塞这两组,本质上都是对是否阻塞的区分,只是阻塞的位置不一样
      • 对应的 阻塞位置 可参考下文 BIO 模型中红线和红框
      • 只要能正确理解和区分这两处阻塞并可以在交流过程中表达清楚,怎么称呼实际上无所谓
    • 这两处阻塞的冠名为什么会有差异
      • 红线 处,是 API 调用相关的阻塞
        • 在日常项目中,我们通常用 同步/异步 进行区分
          依据如 Ajax 是一门用于网页异步请求的技术来,小伙,把这个接口给我改成异步
        • 但在官方的 API 中,确实把它叫做 阻塞/非阻塞
          依据如 socketServerChannel.configureBlocking(false)
      • 红框 处,是 系统底层读写层面上的阻塞
        • 在日常项目中,我们通常用 阻塞/非阻塞 进行区分
          依据如 测试反馈迁移大镜像总卡死,我看了下,后台阻塞了,小伙你给优化一下
        • 但在官方的 API 中,确实把它叫做 同步/异步
          依据如 AsynchronousFileChannelDemo
          这个类在读写时,是系统读写完成后通过回调通知 API 的,其名为 Asynchronous 而不是 Unblocked

    §3 各个 IO 模型

    BIO

    • 主线程 收到 IO 请求
    • 主线程 在 recvfrom 系统调用阻塞
      • 应用因为系统调用切换至内核态
      • 内核等待数据就绪,比如等待网络 IO 中完整数据包到达
      • 内核数据就绪,复制到用户空间
      • 完成复制,退回用户态
    • 应用处理数据
    • 返回 IO 响应

    在这里插入图片描述
    阻塞位置
    这里的阻塞就是指广义的阻塞(不在区分阻塞、异步),下同
    下面方法会互相阻塞,卡在其中之一就不能响应其他

    • accept(),等待连接
    • read(),等待对方写
    • write(),等待对方读
      因此,SocketServer 等待对方 Socket 写出时,就不能响应其他 Socket 的连接、写出请求

    PIO

    • 主线程 收到 IO 请求
    • 主线程 在线程池启动一根 子线程 处理此 IO 请求
    • 子线程 在 recvfrom 系统调用阻塞
      • 子线程 因为系统调用切换至内核态
      • 内核等待数据就绪,比如等待网络 IO 中完整数据包到达
      • 内核数据就绪,复制到用户空间
      • 完成复制,退回用户态
    • 子线程 处理数据
    • 返回 IO 响应

    在这里插入图片描述

    阻塞位置
    下面方法 对同一个线程而言 会互相阻塞,卡在其中之一就不能响应其他

    • accept(),等待连接
    • read(),等待对方写
    • write(),等待对方读

    PIO 达到了非阻塞效果

    • 阻塞的实际上是线程池里开辟处理处理请求的子线程
    • 主线程上的 SocketServer 不耽误接待其他 Socket 的请求
      由此达到了 非阻塞 效果,但上图红线处的阻塞实际上并没有解决,因此只是 伪非阻塞

    伪非阻塞的问题
    伪非阻塞通过开辟线程达到非阻塞的效果,但它的问题也来自于线程池

    • 开辟线程本身是个系统调用,会加剧模型中的用户态内核态切换,带来开销
    • 线程本身也是一种比较珍贵的资源,JVM 线程数是有限制的
      Linux 默认一个线程最大 1024 个线程,详情参考 基础 | JVM - [内存溢出]
    • 开辟线程本身也是一种开销,需要消耗 CPU 时间
    • 高并发时,线程池打满只能拒绝请求

    NIO

    • 客服端通过 Socket 连接到服务端,并发送请求
    • 服务端将请求和文件描述符注册到 多路复用器
    • 多路复用器 将文件描述符和请求按请求与对应的 事件处理器进行关联
    • 多路复用器 默认通过 epoll 函数监管文件描述符和请求
    • 数据就绪时,多路复用器生成对应的 文件事件
    • 多路复用器文件事件 放到 事件队列
    • 事件队列 经由单线程的 文件事件分派器 依次消费
    • 文件事件分派器 将事件分发给关联的 事件处理器 进行处理

    阻塞位置
    下面方法都是 不阻塞不完全阻塞

    • accept()完全不阻塞,没有连接直接返回 error
    • read()不完全阻塞,未读到数据直接返回error,只有读到数据且正在读数据的过程中阻塞
    • write()不完全阻塞

    NIO 引入 Socket 队列 (假设没有引入多路复用)
    通过了 Socket 队列,实现了单线程监管多个 Socket,同时解决了频繁开辟线程的问题

    • NIO 对多个 Socket 的监管,只依赖于一个线程
    • Socket 的连接加入数组,只由一个线程周期性遍历一次
    • 每次遍历只判断数据是否就绪,不进行其他操作

    Socket 队列尚有下列问题没有解决

    • Socket 很多时,Socket 队列就会很长,但实际上数据就绪的可能很少,这是极大的浪费
      Socket 队列中大部分 Socket 都是没有数据的,但不得不始终轮询
    • 没有多路复用时,只能通过 read 进行轮询,相当于 玩命切换用户态、内核态

    总结:
    优点

    • 实现单线程管理多 Socket
      不足
    • 队列中管理很多 Socket 连接,连接越多,队列越长,但数据就绪数量可能很低,浪费
    • 轮询依赖 read(),海量系统调用导致频繁的用户态内核态切换,浪费

    在这里插入图片描述

    NIO 引入多路复用
    更多详细内容参考 中间件 | Redis - [深度理解多路复用]

    多路复用程序将批量的轮询包装为多个文件描述符(File Descriptor),并统一传输给内核
    轮询通过内核态完成,避免用户态内核态海量切换

    在这里插入图片描述

  • 相关阅读:
    AI芯片软件定义硬件架构
    Nginx HTTP框架综述
    X86架构和X64架构有什么区别?
    [Vue项目实战]尚品汇 -- 初始化项目以及项目的配置与分析
    科力信息:智能交通“新基建”借CRM搭乘数字化快车
    适合编程初学者的开源博客系统(Flutter版)
    实用电脑软件
    蓝桥杯每日一题2023.9.21
    Python3.10动态修改Windows系统(win10/win11)本地IP地址(静态IP)
    Spring中shutdown hook作用
  • 原文地址:https://blog.csdn.net/ZEUS00456/article/details/127038503