欢迎访问shiker.tech

请允许在我们的网站上展示广告

您似乎使用了广告拦截器,请关闭广告拦截器。我们的网站依靠广告获取资金。

NIO如何解决阻塞问题?
(last modified Aug 9, 2023, 10:58 PM )
by
侧边栏壁纸
  • 累计撰写 182 篇文章
  • 累计创建 64 个标签
  • 累计收到 4 条评论

目 录CONTENT

文章目录

NIO如何解决阻塞问题?

橙序员
2023-08-09 / 0 评论 / 0 点赞 / 355 阅读 / 2,069 字 / 正在检测百度是否收录... 正在检测必应是否收录...
文章摘要(AI生成)

NIO通过使用Selector和多路复用器的概念,有效地解决了BIO中线程开销过大的问题。在BIO中,每个客户端连接对应一个线程,导致服务端需要开启大量线程处理连接请求,造成巨大的资源消耗。而NIO中,引入了Selector来负责检查所有连接的就绪状态,通过轮询注册在其上的通道,获取就绪状态的通道进行处理。这样一来,一个Selector可以轮询多个通道,大大减少了线程的数量,提高了资源利用率。同时,NIO中使用了缓冲区来存储请求数据,可以在任务完成后发送通知,避免了连接超时的问题。总之,NIO通过Selector和缓冲区的方式,有效地解决了BIO中的线程开销问题,提高了系统的并发性能。

NIO概念

结合上篇文章BIO中所述的,BIO中“一个客户端(连接)对应一个线程,只有一个服务线程为每个连接分配处理线程”:

image-20230809202325710

这种客户端与服务端1:1的处理方式,会导致服务端产生很大的线程开销,那么NIO是如何解决这个问题的呢?

包裹分拣

上面这个问题很像一个流水线问题,即上游有6条流水线在源源不断的给你产生包裹,按照阻塞式IO的思路,有多少个请求,你就应该开启多少个线程处理。现在有6条流水线,那你就应该安排6个工人来处理。

但是你的资金有限,雇不起6个工人来处理包裹,只能雇佣到2个人来做分拣工作:

image-20230809204057769

这时候该怎么办呢?

一种思路时让两个人分分工,一个人处理3条流水线,这种处理方式就是伪异步IO的处理方式:

image-20230809210238365

这种方式一看就有弊端:工人分拣缓慢时会导致其他流水线阻塞(上图3、6流水线就阻塞了),这样就导致流水线无法正常工作。那么对于请求处理时,我们通过线程池的方式来处理请求,读操作缓慢时同样会导致多个输入流阻塞,阻塞队列积满后后入列操作无法进行,最终导致大量连接超时。

NIO给我们的思路是,我们安排一个人来负责定时检查所有流水线上是否有包裹,有则取出来交给另一个人来专门进行分拣工作:

image-20230809204523136

这样我们的流水线不会因为我们工人分拣能力而阻塞,如果我们缓冲区(放包裹的地方)够大,那么完全可以处理完这些请求,而在最终任务完成后发送通知。

概念小结

从包裹分拣的问题中我们可以看到,相比于BIO不计成本的创建线程,NIO在限定的资源内就能处理大量的请求。其中负责检查流水线包裹的是我们的Selector(即多路复用器),它提供了选择已经就绪的任务的能力,通过不断轮询注册在其上的channel,获取就绪状态的channel。一个多路复用器可以轮询多个channel,没有最大连接句柄的限制。而我们分拣的工人则是对应各个通道。

image-20230809225441748

NIO原理

了解了NIO是什么之后,我们来看下NIO在各个阶段都做了什么,与BIO相同,把大象装冰箱的步骤是一致的,但是由于增加了selector的概念,具体的流程也就不同。

服务端创建

image-20230809224450793

服务端流程创建示意如上:其主要是创建服务端通道,并将通道注册到selector上。

服务端创建即服务端通道创建,首先是创建selector,最终返回的结果是WindowsSelectorImpl,及windows下的selector的多线程实现版本,它包含了:
1、一个selectionKey集合
2、一段系统内存-系统缓冲区
3、一个唤醒管道,用来在select阻塞时进行唤醒
4、一个选择线程集合

唤醒管道的建立是在服务端建立一个本地回环连接(服务端的端口连接服务端所在机器的另一个端口),这样调用唤醒方法时,能够中断阻塞的select方法。

selectionKey则是由一个通道+通道对应的selector组成,除此之外selectionKey 还包括其在selector中的下标。下面流程中所有通道注册包括以下几步:

  1. selector的通道列表中添加此通道

  2. 设置此通道在selector中的下标

  3. 为此通道分配缓冲区空间

  4. 通道状态设置为接收就绪

另外,在通道注册到selector时,如果selector的总通道数已满,则会对通道数进行扩容:

  1. selector的总通道数变为原来的2倍
  2. 轮询线程总数+1

建立连接

image-20230809213454818

在服务端完成建立后,我们建立客户端时,会连接创建好的服务端,这时服务端首先接收到连接请求写入缓冲区。

当selector轮询缓冲区发现服务端通道发生数据变更时,则调用服务端通道进行接受连接。并在服务端为响应的客户端建立客户端通道,用于处理之后此客户端的请求。

这里每个通道都有一个文件描述符,即缓冲区指针。selector会调用系统的poll方法进行轮询,如果对应缓冲区发生变更,则更新对应通道的标记位。并将其放入到变更的通道列表中,这样我们的就能通过遍历变更的通道列表,对其调用accept方法进行请求受理。

请求发送和响应

image-20230809214100146

连接建立完成后,服务端和客户端都以为这个连接建立好了对应的通道。我们在客户端发起请求后,服务端对应的客户端通道就会产生响应的数据变更。通过轮询我们就能发现对应通道,我们便能在服务端对所有变更的管道做统一的分发处理。

轮询&唤醒

轮询的操作是较为复杂的。其中还包括了唤醒操作。为什么需要唤醒呢?我们首先来看下轮询是怎么实现的:

image-20230809220215821

从上图可知,轮询主要做了如下几步:

  1. 由于一次轮询得到的是当前时间点所有变更的管道,所以整个选择流程是阻塞的
  2. 如果有取消的管道,则需要对管道进行注销。
  3. 如果上次轮询通过唤醒中断了,则触发空轮询讲中断复位
  4. 分配轮询线程,进行并发轮询
  5. 获取各线程轮询结果,更新通道状态和变更通道列表

轮询的逻辑,主要是对缓冲区的检测,我们的通道主要是一些读写指针,如果某个通道的读写指针的位置和原纪录的指针位置不同,则说明该通道发生了读写操作,需要放到selectKeys中

不同缓冲区的轮询线程不同,所有通道的变化是并发监听的,但是当所有线程的轮询全部完成后,才会更新通道状态。
如果轮询线程发现当前轮询次数不变,则进行等待,等待其他线程

通道注销

注销包括以下几步:

  1. selector的通道列表中删除此通道
  2. 设置此通道的selector下标置为-1
  3. 删除此通道缓冲区空间
  4. 通道状态设置为关闭
  5. 关闭此通道

轮询线程分配

如果轮询线程总数大于轮询线程数,则增加轮询线程;

如果轮询线程总数小于轮询线程数,则减少轮询线程

在通道注册到selector时,如果selector的总通道数已满,则会对通道数进行扩容:

  • selector的总通道数变为原来的2倍
  • 轮询线程总数+1

唤醒

唤醒流程如下,服务端调用唤醒方法时,通过系统发起唤醒请求写入到缓冲区中,缓冲区变更后则被系统poll方法轮循到,此时,轮询中止。返回空的变更通道列表。

image-20230809224415938

0

评论区