前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >NIO/IO多路复用

NIO/IO多路复用

作者头像
shysh95
发布2020-06-03 09:27:23
1.8K0
发布2020-06-03 09:27:23
举报
文章被收录于专栏:shysh95shysh95

NIO 是一种同步非阻塞模型(Non-blocking IO),也是 IO 多路复用的基础。在了解 NIO 之前我们先回顾一下我们传统 IO 的相关知识。

BIO

首先我们通过一段传统的 BIO 代码来进行回顾。

代码语言:javascript
复制
public class BioServer {

    private final ExecutorService executorService;

    private final int port;

    public BioServer(int port) {
        executorService = Executors.newFixedThreadPool(100);
        this.port = port;
    }

    public void startServer() throws IOException {
        ServerSocket serverSocket = new ServerSocket();
        serverSocket.bind(new InetSocketAddress(port));
        while (!Thread.currentThread().isInterrupted()) {
            Socket socket = serverSocket.accept();
            executorService.submit(new IoHandler(socket));
        }
    }


    private static class IoHandler implements Runnable {

        private Socket socket;

        public IoHandler(Socket socket) {
            this.socket = socket;
        }

        @Override
        public void run() {
            try {
                while (!Thread.currentThread().isInterrupted() && !socket.isClosed()) {
                    // 读取数据
                    String data = readData(socket.getInputStream());
                    // 处理数据
                    String response = handlerData(data);
                    // 写入数据
                    writeResponse(socket.getOutputStream(), response);
                }
            } catch (Exception e) {
                System.out.println("io handler occur error. " + e.getMessage());
            }
        }

        private void writeResponse(OutputStream out, String data) throws IOException {
            out.write(data.getBytes());
        }

        private String handlerData(String data) {
            System.out.println(data);
            return data;
        }

        private String readData(InputStream input) throws IOException {
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(input));
            StringBuilder builder = new StringBuilder();
            String line;
            while ((line = bufferedReader.readLine()) != null) {
                builder.append(line);
            }
            return builder.toString();
        }
    }
}

上述代码是我们传统的 SockerServer,是一种阻塞性的 IO。之所以称他为阻塞性 IO 是因为接收连接,读取数据、写回数据都会阻塞我们的线程。上述代码我们开启了一个线程池用来处理 Server 与客户端连接建立后的操作,这样可以避免我们的主线程一直阻塞只能处理单个连接。上述模型的主要缺点就是:

  • 过于依赖线程
  • 线程的创建和销毁成本很高,在 Linux 这种系统中线程本质上就是一个进程。创建和销毁都是重量级的函数
  • 线程本身就很占内存,如果系统中的线程数过多,将会占用大量的 JVM 内存
  • 线程切换成本很高,操作系统在进行线程切换时需要保留线程的上下文,然后执行系统调用。如果线程数过高,线程切换的时间可能会大于线程执行的时间,往往会造成 cpu load 过高。
  • 系统负载过高,如果客户端网络环境不稳定,回传速度变慢,那么将会造成大量线程阻塞,从而活动线程数明显增高,增大系统负载压力
IO 模型对比

IO 操作其实就分为两个步骤,等待和操作。等待的意思其实就是等待连接建立,等待数据可读,等待数据可写,而操作则指的是读数据写数据。

微信截图_20200530215806.png

上述图很好的说明当下 5 种 IO 模型的阻塞特点。

  • 传统 IO(阻塞 I/O)会阻塞所有的操作
  • 非阻塞 I/O(NIO)在等待阶段不会进行阻塞,但是在操作阶段依然阻塞。非阻塞 IO 会不停的检查数据是否就绪,如果就绪则进行操作,但是这样会有一个缺点就是这个检查的时机你怎么控制,因为这些等待就绪的时间点我们是无法确定的,如果有多个 IO 那么我们需要一一进行检查会发生线程上下文的切换
  • IO 多路复用其实就是基于 NIO 的基础上加入了事件机制,程序会注册一组 socket 文件描述符给操作系统,然后监视这些 fd 是否有 IO 事件发生,如果有,程序会被通知,IO 多路复用的方式主要有 select、poll、epoll,这三个函数都会进行阻塞,所以可以放在 while(true)循环里使用,不会造成 CPU 的空转
  • 信号驱动式 I/O 是指进程预先告知内核,使得当某个描述符上发生某个事件时,内核使用信号通知相关进程
  • 异步 IO 不但等待就绪时非阻塞的,数据从网卡到内存的过程(操作)也是异步的
IO 多路复用
Select
代码语言:javascript
复制
int select (int n, fd_set *readfds, fd_set *writefds, fd_set 
*exceptfds, struct timeval *timeout);

select 只有一个函数,调用 select 时,需要将监听句柄和最大等待时间作为参数传递进去,select 会发生阻塞,直到一个事件发生了,或者等到最大 1 秒钟(tv 定义了这个时间长度)就返回。select 主要有以下缺点:

  • select 返回后要挨个遍历 fd,找到被“SET”的那些进行处理
  • select 是无状态的,即每次调用 select,内核都要重新检查所有被注册的 fd 的状态。select 返回后,这些状态就被返回了,内核不会记住它们;到了下一次调用,内核依然要重新检查一遍。于是查询的效率很低
  • select 能够支持的最大的 fd 数组的长度是 1024
poll
代码语言:javascript
复制
int poll (struct pollfd *fds, unsigned int nfds, int timeout);

poll 优化了 select 的一些问题,参数变得简单一些,没有了 1024 的限制。缺点:

  • 依然是无状态的,性能的问题与 select 差不多一样
  • 应用程序仍然无法很方便的拿到那些有事件发生的 fd,还是需要遍历所有注册的 fd
epoll
代码语言:javascript
复制
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, 
int timeout);

使用 epoll 时,需要先使用函数 epoll_create()在内核层创建了一个数据表,接口参数是一个表达要监听事件列表的长度的数值(会动态改变的)。这样一来,不用每次监听都要传一遍 fd(传递 fd 会导致 fd 数据从用户态复制到内核态)。创建完数据表,就可以使用另外一个函数 epoll_ctl()来管理数据表,对监听的 fd 执行增删改操作。最后再调用 epoll_wait()方法等待事件的发生。这样做的优点是:

  • select 和 poll 每次都需要把完成的 fd 列表传入到内核,迫使内核每次必须从头扫描到尾。而 epoll 完全是反过来的。epoll 在内核的数据被建立好了之后,每次某个被监听的 fd 一旦有事件发生,内核就直接标记之。epoll_wait 调用时,会尝试直接读取到当时已经标记好的 fd 列表,如果没有就会进入等待状态。
  • epoll_wait 直接只返回了被触发的 fd 列表,这样上层应用写起来也轻松愉快,再也不用从大量注册的 fd 中筛选出有事件的 fd 了。
Reactor

通过 IO 多路复用我们可以实现一个线程处理多个 IO 操作,虽然单线程 IO 效率很高,没有上下文切换,但是在实际使用中单线程不可能满足我们的需求,后面就延伸出了 Reactor 模型,这个下节讲述。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-05-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序员修炼笔记 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • BIO
  • IO 模型对比
  • IO 多路复用
    • Select
      • poll
        • epoll
        • Reactor
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档