Redis之IO线程、IO多路复用,BIO、NIO和AIO区别
参考文章:https://blog.****.net/Happy_wu/article/details/80052617
redis的线程
redis是单线程操作的,但是却可以处理高并发。原因是基于多路复用的非阻塞IO,基于NIO(non_blocking_io);
redis为什么这么快?
- 完全基于内存,绝大部分请求是纯粹的内存操作;
- 数据结构简单,对数据操作也简单,redis中的数据结构是专门进行设计的;
- 采用单线程,避免了不必要的上下文切换和竞争条件,不用考虑加锁释放锁和死锁的问题;
- 使用多路复用模型,非阻塞IO;
对多路复用的理解
多路Io复用是利用select、poll、epoll可以同时监察多个流的IO事件的能力,在空闲的时候会把当前线程阻塞,当有一个或多个流由IO事件发生时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll只是轮询那些真正发出了时间的流),并且一次顺序的处理就绪的流。
- select:服务端一直在轮询、监听如果有客户端链接上来就创建一个连接放到数组A中,继续轮询这个数组,如果在轮询的过程中有客户端发生IO事件就去处理;select只能监视1024个连接(一个进程只能创建1024个文件);而且存在线程安全问题;
- poll:在select做了许多修复,比如不限制监测的连接数;但是也有线程安全问题;
- epoll:也是监测IO事件,但是如果发生Io事件,它会告诉你是哪个连接发生了事件,就不用再轮询访问。而且它是线程安全的,但是只有linux平台支持;
IO多路复用(Reactor)
实际上虽然上述方式允许单线程内处理多个IO请求,但是每个IO请求的过程还是阻塞的,平均时间甚至比同步阻塞IO模型还要长,而IO多路复用模型使用了Reactor设计模式实现线程不必阻塞,可以干别的事情,当IO事件触发时,再进行处理。
EventHandler
表示IO事件处理器,它拥有IO文件句柄Handle以及对Handle的操作handle_event。Reactor用于管理EventHandler进行注册删除,并使用handle_events实现事件循环,不断调用同步时间多路分离器的多路分离函数select,只要某个文件句柄被**,select就返回(阻塞),handle_events就会与文件句柄关联的事件处理器的handle_event进行相关操作。
将用户线程轮询的IO操作状态的工作统一交给handle_events事件循环进行处理,用户线程注册事件处理器之后可以继续执行其他工作,而Reactor线程负责返回select函数检查socket状态,当有socket被**石,则通知相应用户线程执行handle_event进行数据读取处理工作。
如何结合事件模型使用NIO同步非阻塞特性
在NIO中如果一个连接不能读写,我们可以把这件事记下来,记录方式是在Selector上注册标记位,然后切换到其他就绪的连接继续进行读写。
NIO中的主要事件: 读就绪、写就绪、有新的连接到来;
我们首先需要注册当这几个时间到来的时候所对应的处理器,然后在合适的时机告诉事件选择器:我们对这个事件感兴趣。
对于写操作,就是写不出去的时候对写事件感兴趣;
对于读操作,就是完成连接和系统没有办法承载新读入的数据时;
对于accept,一般是服务器刚启动时;
对于connect,一般是connect失败需要重新连接或者直接异步调用时;
对BIO、NIO和AIO的概念解析
所有的系统IO都分为两个阶段:等待就绪和操作。例如:读函数分为等待系统可读和真正的读;同理,写函数分为等待网卡可写和真正的写。
可以将这三种IO的特点概括为:
- BIO里用户最关心"我要读";我要一直等在这处理处理时间;
- NIO里用户最关心"我可以读了";我不在这等了,如果有事件发生,你就通知我,我再来处理;
- AIO里用户最关心"读完了";你帮我处理吧,处理完了通知我。
BIO
Java BIO : 同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
每一个线程守着一个IO通道,当没有IO时这个线程就阻塞着,一旦有IO事件发生,这个线程就开始工作
缺点
严重依赖线程:
- 线程的创建和销毁成本很高;
- 线程本身占用较大内存;
- 线程切换成本很高;
- 容易造成锯齿状的系统负载;
Java NIO : 同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。socket主要的读、写、注册和接受函数,在等待就绪阶段都是非阻塞的,真正的IO操作是同步阻塞的。
将每个连接(IO通道)都注册到Selector多路复用器上,告诉复用器我已经连接了,如果有IO事件发生,你就通知我。Selector就不断地调用select函数,去访问每一个通道看那个通道有IO事件发生,如果发生了就通知,内核开启一个对应事件的线程去工作
Java AIO(NIO.2) : 异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。当进行读写操作时,只须直接调用API的read或write方法即可。这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序。 即可以理解为,read/write方法都是异步的,完成后会主动调用回调函数。
Reactor和Proactor
Reactor模式是基于同步IO的,而Proactor模式是基于异步IO相关的
Reactor
- 某个事件处理者宣称它对某个socket上的读事件很感兴趣;
- 事件分离者等待这个时间的发生;
- 当事件发生了,事件分离者被唤醒,负责通知先前那个事件处理者;
- 事件处理着收到消息,于是去那个socket上读数据了;
- 如果需要,它将再次宣称对这个socket上的读事件感兴趣,一直重复上面的步骤。
Proactor
- 事件处理者直接投递发一个写操作;
- 这个时候,事件处理者根本不关心写事件,它只管发这么个请求,它魂牵梦萦的是这个鞋操作的完成事件;
- 它发个命令不管具体的事情了。只等着比人帮他搞定的时候给他回个话;
- 事件分离者等着这个写事件的完成;
- 当事件分离者默默的等待完成事件到来的同时,操作系统已经开始干活了,它从目标读取数据,放入用户提供的缓冲区,最后通知事件分离者,这个事情我干完了;
- 事件分离者通知之前的事件处理者,你吩咐的事情搞定了;
- 事件处理者这时会发现想要读的书已经放在它提供的缓冲区了,想怎么处理都行;
- 如果有需要,事件处理者还像之前一样发起另一个写操作,重复上面的步骤。
NIO存在的问题
- 连接数较小的时候,优势不明显;
- 没有完全屏蔽平台差异,仍然是基于各操作系统的IO系统实现的。建议使用成熟的NIO框架,Netty、MINA等。
对阻塞、非阻塞、同步、异步的理解
参考文章:https://blog.****.net/ty497122758/article/details/78979302
以银行取款为例:
- 同步:自己亲自(用户线程)持银行卡取钱;
- 异步:委托朋友(OS)拿着你的银行卡和密码(数据缓冲区地址和大小)去取钱;
- 阻塞:ATM排队取款,只能等待;
- 非阻塞: 柜台取款,取个号,然后坐在椅子上做其它事,等号广播会通知你办理,没到号你就不能去,你可以不断问大堂经理排到了没有,大堂经理如果说还没到你就不能去(使用非阻塞IO时,如果不能读写Java调用会马上返回,当IO事件分发器会通知可读写时再继续进行读写,不断循环直到读写完成)。
一个IO操作实际上是被分为两步的,就拿处理网络数据来说。第一步:发起IO请求,第二部实际的IO操作。
阻塞IO和非阻塞IO的区别在于第一步;发起IO请求线程是否会被阻塞,如果阻塞直到完成那么就是传统的阻塞IO;否则就是非阻塞IO;
同步IO和非同步IO的区别在于第二步;如果实际的IO读写阻塞请求进程,那么就是同步IO,因此阻塞IO、非阻塞IO、IO复用都是同步IO;如果不阻塞,而是由操作系统帮你做完再将结果返回给你,那么就是异步IO。
当有一个read操作发生时,会经过以下流程
第一步:
- 通过read系统调用想内核发起读请求
- 内核想硬件发送读指令,并等待读就绪
第二步:
3. 内核把将要读取的数据复制到描述符所指向的内核缓冲区中
4. 将数据从内核缓冲区拷贝到用户进程空间中