聊一聊NIO到底优化了什么

​前言

线程是一个勤劳的worker,作为程序设计者需要为每个线程充分准备好运行条件,把握好创建时机,才能让线程时刻处于繁忙状态,充分压榨它的服务能力,从而高效地创造价值。

在互联网时代,网络通信是所有应用的基础。我们常常用一个网络应用每秒钟能够响应多少请求(QPS)来衡量一个系统的并发能力。支撑这种并发能力的关键一环就是网络IO。说道网络IO,JAVA系的同学第一个想到的一定是NIO(New IO)。它是我们日常当中用得最多的网络IO模型,是一种非常高效的网络IO模型(redis中的单线程IO模型就是这种模式)。今天我们就一起来看看NIO究竟高效在哪里,让我们一起来看下网络IO模型的演化过程。

单线程Socket通信

有过网络编程经验的同学都知道socket是网络编程中的基础对象。在网络通信时,无论是服务端还是客户端都需要先创建socket对象,通过socket对象与对方进行网络交互。socket通信的主要交互过程如下:

                          

这是一次客户端与服务端进行通信的交互过程。大家可以发现在服务端程序中接受请求的accept()调用和接受数据的recv()调用都是阻塞调用。阻塞是什么意思那,就是你的线程罢工了,不干活了。有人会说,反正也没有有效的请求阻塞是正确的。那么试想一下,当多个客户端同时访问时,服务端在与当前socket通信的时候就无法与后续的socket进行通信。也就是说服务端的并发能力是1。这是所有的系统都无法接受的。

那么如何来破解这个问题那,最直接的想法就是加多线程,充分利用操作系统的并发能力。

多线程Socket通信

由于通信接口的调用会阻塞当前线程,那么我们通过多线程的扩展,使用一个线程来为一个socket连接服务,就可以获得同时对多个客户端进行服务的能力。多线程socket通信的交互过程如下:

从上图可以看出,服务端的响应过程可以分为两个步骤:

  • 请求接受阶段(接受线程专门负责接受)

  • 请求处理阶段(针对每个请求启动独立线程进行响应)

在请求接受阶段,通过独立线程进行accept调用,当接受到新的请求之后就启动新的请求处理线程进行数据接受,业务处理和数据返回。通过上面的响应步骤拆分和多线程引入之后,服务端可以同时响应多个客户端请求了。似乎问题得到解决,那么我们来看下高并发的场景。当同时存在大量的客户端向服务端进行服务访问时,那么服务端就需要同等数量的线程对客户端进行请求处理。每个线程都需要分配独立的线程栈,一定数量的寄存器。海量的线程占用的资源对机器资源的消耗,以及操作系统为这么多的线程调度计算时间分片所花费的开销是不可接受的。所以问题不应该通过增加线程进行一对一的VIP服务进行解决,而是应该尽量的提高单线程的服务能力,让一个线程能够更快更多更好的服务客户。可是我们上面已经提到,服务端在调用的过程中存在阻塞操作,单线程无法同时服务两个客户端。那么让我们来具体看一下单次通信的具体过程,一起看看服务端为什么要阻塞。

单次通信过程

服务端程序运行调用过程如下:

可以看到,整个通信过程中大部分的交互是由操作系统完成的。整个通信过程中用户程序只要在用户空间和内核空间进行数据拷贝。可以说这是一个非常轻量的工作了。也就是说服务端响应线程中大部分的时间都是在等待操作系统完成数据通信。这种等待是非常昂贵的,作为一个勤劳的眼里有活儿的线程,我完全可以利用这个时间去服务那些已经完成底层数据通信的socket,没必要在这里傻等。

NIO的优化逻辑

对于线程来说,因为他服务的socket没有准备好数据,所以没活儿可干从而造成的等待(并不是我想偷懒)。可以说这是由于socket通信状态不透明导致的工作线程启动过早,从而造成了浪费(客户连接成功就启动工作线程等候了)。那么问题来了,我怎么知道客户什么时候READY了,需要享受服务了那。

只有操作系统知道,所以只能问操作系统。但是在高并发场景下,操作系统管理了大量的socket,如果你贸然去询问,操作系统只能将所有的socket轮询一遍,显然这不是一个高效的沟通。为了能够进行高效友好的沟通,我们的用户程序可以提前告诉操作系统,用户程序对哪个socket的哪种状态(主要两种ACCEPT和READ)感兴趣,这样操作系统就能够在通信过程中有针对性的收集(通过将事件与网卡驱动程序的回调函数关联,让网卡驱动程序通过回调函数告知操作系统)。

通过这样的告知,收集,询问机制,我们的用户程序就能够准确的知道当前哪些socket立刻需要被服务,从而为我们的服务线程提供精准调度。让他们的每次启动都处于有事可做的状态,大大提高单个线程的有效服务能力。这就是NIO的优化机制,用户线程先把感兴趣的socket和对应的状态(event)告诉(register)操作系统,然后通过一个询问对象(selector)向操作系统查询(select)就绪事件。获取到就绪事件后再启动工作线程进行服务。其调用过程如下:

NIO程序通过注册机制向操作系统注册自己感兴趣的事件,ACCEPT和READ。操作系统将这些事件与网卡驱动程序建立回调关联关系。当相应事件发生时,驱动程序会通过回调函数告知操作系统,并将就绪事件统一存放到一个的链表中。当用户程序调用select函数时,操作系统会将就绪事件返回给用户程序。由多线程对就绪事件进行统一的处理。通过上述运行过程描述可以看出,NIO的优化主要集中在以下两个方面:

  • 事件注册与就绪收集。通过事件机制,充分利用了下层系统的探测和收集能力(操作系统和驱动程序)。使得用户感兴趣的事件能够精准收集,为上层线程能够精准调度提供信息支撑。

  • 事件并行处理。当用户调用select接口时,能够获取到所有就绪的用户事件。用户可以通过多线程来对这些事件进行并行处理。从而实现了客户端请求的并行响应(这些响应线程不会产生阻塞,一直产出有效计算)。

NIO线程模型如下:

通过这样的线程模型优化,NIO就能够让所有的工作线程工作时满负荷运转,消除了无效的线程阻塞,提高了线程的利用率,从而提高整机的服务能力。以上就是个人对NIO的一些理解,欢迎大家一起讨论,附上实验程序:https://github.com/SharkWater/blog.git

 

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值