当前位置: 首页 > 科技观察

阻塞、非阻塞、多路复用、同步、异步,BIO、NIO、AIO一锅终端

时间:2023-03-20 12:07:33 科技观察

本文转载自微信公众号“sowhat1412”,作者sowhat1412。转载本文请联系sowhat1412公众号。继上面的操作系统之后,IO会涉及到阻塞、非阻塞、多路复用、同步、异步、BIO、NIO、AIO等几个知识点。知识点虽然不难,但往往容易混淆,所以在此标记一下,以示鼓励。1阻塞与非阻塞1.1阻塞BlockingIO在阻塞IO的情况下,当用户调用read时,用户线程会被阻塞,直到内核数据准备好,数据从内核缓冲区copy出来,read才会返回到用户模式缓冲区。可以看出块有两部分。CPU将数据从磁盘读取到内核缓冲区。CPU将数据从内核缓冲区复制到用户缓冲区。1.2Non-blocking非阻塞IO非阻塞IO发送读请求后,发现数据没有准备好,会继续执行。这时候应用程序会不断的轮询pollingkernel询问数据是否就绪。当数据没有准备好时,内核立即返回EWOULDBLOCK错误。直到数据被复制到应用程序缓冲区中,读请求才会得到结果。你应该注意!最后一个read调用获取数据是一个同步过程,也是一个需要等待的过程。这里的同步指的是将内核态的数据复制到用户程序的缓冲区中的过程。1.3IO多路复用在非阻塞IO多路复用的情况下,当没有数据可用时,应用程序每次都会轮询内核,看数据是否就绪,消耗CPU。能不能避免轮询,当内核缓冲数据Ready时,使用事件通知机制通知应用进程数据准备好了?应用进程在没有收到数据就绪事件通知信号时可以忙于写其他工作。这时IO多路复用就派上用场了。中文的IO多路复用是个很头疼的问题。IO多路复用的原文叫做I/O多路复用。这里的多路复用实际上是指单个线程同时记录和跟踪每个Sock(I/O流)的状态。管理多个I/O流。它的发明是为了最大化服务器吞吐量。实现一个线程监听多个IO请求,哪个IO有请求,就从内核拷贝数据到进程缓冲区。复制期间,被封杀!现在可以通过mmap地址映射的方式达到内存共享的效果,避免真拷贝。提高效率。IO多路复用像select、poll、epoll都是I/O多路复用的具体实现。1.3.1selectSelect是IO多路复用的第一个版本,提出后暴露了很多问题。select会修改传入的参数数组,这对于一个需要多次调用的函数来说是非常不友好的。select如果任何一个sock(I/O流)有数据,select只会返回,不会告诉哪个sock有数据,只能自己遍历查找。select只能监控1024个链接。选择不是线程安全的。如果加了一个sock进行select,突然另一个线程发现sock没有用,需要撤掉。此选择不支持它。1.3.2pollpoll修复了select的很多问题。poll删除了1024个连接的限制。设计轮询不再修改传递的数组。但是poll仍然不是线程安全的,这意味着无论服务器多么强大,你也只能在一个线程中处理一组I/O流。当然你可以用多进程来配合,但是这样你就有了多进程的各种问题。1.3.3epollepoll可以说是I/O多路复用的最新实现。epoll修复了poll和select的大部分问题,例如:epoll现在是线程安全的。现在epoll不仅告诉你sock组里的数据,还会告诉你哪个sock有数据,不用你自己找了。epoll内核模式管理各种IO文件描述符。以前是用户态把所有的文件描述符都发给内核态,然后内核态负责过滤返回可用的数组。现在epoll模式下,所有的文件描述符都保存在内核态,查询时不使用。传入文件描述符。1.3.4三种方式对比图横轴Deadconnections表示连接数。之所以叫deadcon,是因为它的测试工具。纵轴是每秒处理的请求数。可以看出epoll每秒处理的请求数基本不会随着链接数的增加而减少。poll和/dev/poll很惨。但是epoll有个致命的缺点就是只有linux支持。比如Nginx为什么通常可以支持4WQPS,就是因为它会在目标平台上使用最高效的I/O复用模型。1.4AsynchronousIOAsynchronousIO然后你会发现上面说的操作并不是真正的异步,因为两个阶段总是要等待一段时间!真正的异步I/O是在内核数据就绪的时候,从内核态复制数据,不需要等待两个进程进入用户态。好在Linux已经为我们准备了aio_read和aio_write函数,实现真正的异步,当用户发起aio_read请求时,它会自动返回。内核会自动将数据从内核缓冲区复制到用户进程空间,应用进程什么都不关心。2同步与异步2.1同步与异步同步与异步的区别在于数据是否由用户线程从内核空间复制到用户空间,分为同步阻塞和同步非阻塞。同步阻塞:此时一个线程维护一个连接,线程完成从数据到读写和处理的整个过程。读写数据时线程被阻塞。同步非阻塞:非阻塞是指用户线程发送读请求后,读请求不会阻塞当前用户线程,但用户线程仍需不断主动判断数据是否准备就绪。这时候它仍然会阻塞等待内核拷贝数据给用户进程。它和同步BIO的区别在于它使用一个连接来等待整个过程。我们以同步非阻塞为例。如下所示,从内核空间向用户空间复制数据的过程是通过用户线程阻塞完成的。同步非阻塞2.2异步对于异步,用户读取或写入后,立即返回,内核完成数据读取和复制工作,完成后通知用户,并执行回调函数(用户提供的回调),此时数据已经从内核拷贝到用户空间。用户线程只需要处理数据,不需要关注读写。用户不需要等待内核复制数据。通知用户时,数据已复制到用户空间。下面以真正的异步非阻塞为例。异步IO可以发现,用户调用后会立即返回,内核会完成数据拷贝工作,并通知用户线程进行回调。2.3同步和异步的比较在同步attention的消息通信机制中,当调用时,直到得到结果后调用才会返回。但是一旦调用返回,你就会得到返回值。也就是说,由调用者主动等待本次调用的结果。异步注意消息通信机制异步通信,调用发出后,调用直接返回,所以没有返回结果。换句话说,当发出异步过程调用时,调用者不会立即得到结果。相反,在调用发出后,被调用者通过状态和通知通知调用者,或者通过回调函数处理调用。3JavaIO在Java中,我们使用套接字进行网络通信。IO有三种主要模式,具体取决于内核支持的模式。BIO:同步阻塞IO。NIO:同步非阻塞IO。AIO:异步非阻塞IO。3.1BIO同步阻塞IO。对于客户端的每一个Socket连接请求,服务端都会有一个处理线程与之对应。未分配给处理线程的连接将被阻塞或拒绝。相当于一个连接一个线程。BIOBIO的特点:使用一个独立的线程来维护一个socket连接。随着连接数的增加,会对虚拟机造成一定的压力。使用流读取数据,流是阻塞的,当没有可读/可写数据时,线程等待,会造成资源浪费。3.1.1BIO示例常量:publicclassConstant{publicstaticfinalStringHOST="127.0.0.1";publicstaticfinalintPORT=8080;}主类:publicclassClientMain{publicstaticvoidmain(String[]args){//开启服务System.out.println("开启服务,监听端口:“+常量。PORT);newThread(newServerThread()).start();//创建socket客户端,发起请求System.out.println("client,requestconnection,andsenddata");try{Socketsocket=newSocket(Constant.HOST,Constant.PORT);//新开一个线程处理socket连接newThread(newClientProcessThread(socket)).start();}catch(IOExceptione){e.printStackTrace();}}}Serverlisteningthread://启动服务监听线程,当收到连接请求时,启动一个新的线程进行处理Socketsocket=serverSocket.accept();newThread(newServerProcessThread(socket)).start();//开启一个新线程进行连接请求处理}}catch(IOExceptione){e.printStackTrace();}}}服务器处理线程:importjava.io.*;importjava.net.Socket;/***服务端收到连接请求后,处理请求的线程,阻塞IO*/publicclassServerProcessThreadimplementsRunnable{私有套接字套接字;publicServerProcessThread(Socketsocket){this.socket=socket;}@Overridepublicvoidrun(){//获取客户端的数据并写回//等待响应="";StringrequestStr="";System.out.println("Datafromclient:");//读取客户端数据while((line=bufferedReader.readLine())!=null){requestStr+=line;System.out.println(line);}//从服务器向客户端发送数据Writerwriter=newOutputStreamWriter(socket.getOutputStream());writer.write("datafromserver"+requestStr+"\r\n");writer.flush();writer.close();bufferedReader.close();socket.close();}catch(IOExceptione){e.printStackTrace();}}}Client:/***维护客户端socket连接线程,阻塞IO*/publicclassClientProcessThreadimplementsRunnable{privateSocketsocket;publicClientProcessThread(Socketsocket){this.socket=socket;}@Overridepublicvoidrun(){//写入数据,等待响应,输出响应StringrequestStr="datafromclient\r\n";try{Writerwriter=newOutputStreamWriter(socket.getOutputStream());writer.write(requestStr);writer.flush();socket.shutdownOutput();//等待响应BufferedReaderbufferedReader=newBufferedReader(newInputStreamReader(socket.getInputStream()));Stringline;System.out.println("服务器响应:");while((line=bufferedReader.readLine())!=null){System.out.println(line);}writer.close();bufferedReader.close();socket.close();}catch(IOExceptione){e.printStackTrace();}}}输出结果:3.2NIO同步非阻塞IONIO:服务端保存一个Socket连接列表,然后轮询这个列表,如果发现某个Socket端口上有数据可读,则表示准备好读取,然后调用socket连接对应的读取操作。如果某个端口的Socket连接已经中断,则调用相应的析构方法关闭该端口。这样就可以充分利用服务器资源,大大提高效率。在执行IO操作请求时,使用一个线程来处理,也就是一个线程一个请求。Java中使用了Selector、Channel、Buffer来实现上述效果。NIO的每个线程都包含一个Selector对象,相当于一个通道管理器,可以实现一个线程处理多个通道的目的,减少线程的创建次数。远程连接对应一个通道,数据的读写通过缓冲区在同一个通道内完成,数据的读写是非阻塞的。通道创建后,需要在selector中注册,同时需要为通道注册感兴趣的事件(客户端连接服务器事件、服务器接收客户端连接事件、读取事件、写入事件),selector线程需要以循环的方式调用selectorselect函数,直到注册通道中所有感兴趣的事件都发生,否则会被阻塞。然后循环遍历所有感兴趣的就绪事件。以上步骤解决了BIO的两个瓶颈:不必为每个连接单独创建线程。数据读写是非阻塞的。下面对以下三个概念进行简单介绍。JavaNIO由以下三个核心部分组成:选择器:选择器允许单个线程处理多个通道。如果你的应用开启了多个连接(通道),但是每个连接的流量都很低,使用Selector会很方便。要使用Selector,你必须向Selector注册Channel,然后调用他的select方法,这个方法会阻塞,直到一个注册的channel有一个事件就绪。一旦这个方法返回,线程就可以处理这些事件,事件的例子包括传入连接、数据接收等。Channel:基本上所有的IO都以NIO中的Channel开头。频道有点像流。数据可以从通道读取到缓冲区,也可以从缓冲区写入通道。缓冲区:缓冲区本质上是一个可以读写数据的内存块。可以理解为容器对象(包括数组)。该对象提供了一组方法,可以更方便地使用内存块。缓冲区对象有一些内置的机制可以跟踪和记录缓冲区的状态变化。Channel提供了从文件和网络中读取数据的通道,但是要读取或写入的数据必须经过Buffer。有几种类型的通道和缓冲区。下面是JavaNIO中一些主要通道的实现:FileChannelDatagramChannelSocketChannelServerSocketChannel可以看到,这些通道涵盖了UDP和TCP网络IO,还有文件IO。下面是JavaNIO中的关键缓冲实现:ByteBufferCharBufferFloatBufferIntBufferLongBufferShortBuffer在微服务阶段,一个请求可能会涉及多个不同服务之间的跨服务器调用。如果想实现一个高性能的PRC框架进行数据传输,可以使用基于Java的NIO是一个支持长连接、自定义协议、高并发的框架,比如Netty。Netty本身是一个基于NIO的网络框架,它封装了JavaNIO复杂的底层细节,为您提供了简单易用的编程抽象概念。比如Dubbo底层使用Netty。Netty通信方式3.3AIOAIO是异步非阻塞IO,比NIO更进了一步。进程读取数据时,只负责发送和接收指令,数据的准备工作完全由操作系统来处理。4参考IO说:https://blog.csdn.net/u013177446/article/details/65936341关于TCP的解释:https://b23.tv/tMxwQV热门IO:https://www.cnblogs.com/LBSer/p/4622749.html小仙IO:https://t.1yb.co/iEAW