关于java:高性能IO模型浅析之BIONIOAIOIO多路复用-基础介绍

1次阅读

共计 5040 个字符,预计需要花费 13 分钟才能阅读完成。

高性能 IO 模型浅析

服务器端编程常常须要结构高性能的 IO 模型,常见的 IO 模型有四种:

(1)同步阻塞 IO(Blocking IO):即传统的 IO 模型。

(2)同步非阻塞 IO(Non-blocking IO):默认创立的 socket 都是阻塞的,非阻塞 IO 要求 socket 被设置为 NONBLOCK。留神这里所说的 NIO 并非 Java 的 NIO(New IO)库。

(3)IO 多路复用(IO Multiplexing):即经典的 Reactor 设计模式,有时也称为异步阻塞 IO,Java 中的 Selector 和 Linux 中的 epoll 都是这种模型。

(4)异步 IO(Asynchronous IO):即经典的 Proactor 设计模式,也称为异步非阻塞 IO。

同步和异步的概念形容的是用户线程与内核的交互方式:同步是指用户线程发动 IO 申请后须要期待或者轮询内核 IO 操作实现后能力继续执行;而异步是指用户线程发动 IO 申请后仍继续执行,当内核 IO 操作实现后会告诉用户线程,或者调用用户线程注册的回调函数。

阻塞和非阻塞的概念形容的是用户线程调用内核 IO 操作的形式:阻塞是指 IO 操作须要彻底实现后才返回到用户空间;而非阻塞是指 IO 操作被调用后立刻返回给用户一个状态值,无需等到 IO 操作彻底实现。

另外,Richard Stevens 在《Unix 网络编程》卷 1 中提到的基于信号驱动的 IO(Signal Driven IO)模型,因为该模型并不罕用,本文不作波及。接下来,咱们详细分析四种常见的 IO 模型的实现原理。为了不便形容,咱们对立应用 IO 的读操作作为示例。

一、同步阻塞 IO

同步阻塞 IO 模型是最简略的 IO 模型,用户线程在内核进行 IO 操作时被阻塞。

图 1 同步阻塞 IO

如图 1 所示,用户线程通过零碎调用 read 发动 IO 读操作,由用户空间转到内核空间。内核等到数据包达到后,而后将接管的数据拷贝到用户空间,实现 read 操作。

用户线程应用同步阻塞 IO 模型的伪代码形容为:

{read(socket, buffer);

    process(buffer);

}

即用户须要期待 read 将 socket 中的数据读取到 buffer 后,才持续解决接管的数据。整个 IO 申请的过程中,用户线程是被阻塞的,这导致用户在发动 IO 申请时,不能做任何事件,对 CPU 的资源利用率不够。

二、同步非阻塞 IO

同步非阻塞 IO 是在同步阻塞 IO 的根底上,将 socket 设置为 NONBLOCK。这样做用户线程能够在发动 IO 申请后能够立刻返回。

图 2 同步非阻塞 IO

如图 2 所示,因为 socket 是非阻塞的形式,因而用户线程发动 IO 申请时立刻返回。但并未读取到任何数据,用户线程须要一直地发动 IO 申请,直到数据达到后,才真正读取到数据,继续执行。

用户线程应用同步非阻塞 IO 模型的伪代码形容为:

{while(read(socket, buffer) != SUCCESS)
        ;

    process(buffer);

}

即用户须要一直地调用 read,尝试读取 socket 中的数据,直到读取胜利后,才持续解决接管的数据。整个 IO 申请的过程中,尽管用户线程每次发动 IO 申请后能够立刻返回,然而为了等到数据,仍须要一直地轮询、反复申请,耗费了大量的 CPU 的资源。个别很少间接应用这种模型,而是在其余 IO 模型中应用非阻塞 IO 这一个性。

三、IO 多路复用

IO 多路复用模型是建设在内核提供的多路拆散函数 select 根底之上的,应用 select 函数能够防止同步非阻塞 IO 模型中轮询期待的问题。

图 3 多路拆散函数 select

如图 3 所示,用户首先将须要进行 IO 操作的 socket 增加到 select 中,而后阻塞期待 select 零碎调用返回。当数据达到时,socket 被激活,select 函数返回。用户线程正式发动 read 申请,读取数据并继续执行。

从流程上来看,应用 select 函数进行 IO 申请和同步阻塞模型没有太大的区别,甚至还多了增加监督 socket,以及调用 select 函数的额定操作,效率更差。然而,应用 select 当前最大的劣势是用户能够在一个线程内同时解决多个 socket 的 IO 申请。用户能够注册多个 socket,而后一直地调用 select 读取被激活的 socket,即可达到在同一个线程内同时解决多个 IO 申请的目标。而在同步阻塞模型中,必须通过多线程的形式能力达到这个目标。

用户线程应用 select 函数的伪代码形容为:

{select(socket);

    while(1) 
    {sockets = select();

        for(socket in sockets) 
        {if(can_read(socket)) 
            {read(socket, buffer);

                process(buffer);

            }

        }
    }

}

其中 while 循环前将 socket 增加到 select 监督中,而后在 while 内始终调用 select 获取被激活的 socket,一旦 socket 可读,便调用 read 函数将 socket 中的数据读取进去。

然而,应用 select 函数的长处并不仅限于此。尽管上述形式容许单线程内解决多个 IO 申请,然而每个 IO 申请的过程还是阻塞的(在 select 函数上阻塞),均匀工夫甚至比同步阻塞 IO 模型还要长。如果用户线程只注册本人感兴趣的 socket 或者 IO 申请,而后去做本人的事件,等到数据到来时再进行解决,则能够进步 CPU 的利用率。

IO 多路复用模型应用了 Reactor 设计模式实现了这一机制。

图 4 Reactor 设计模式

如图 4 所示,EventHandler 抽象类示意 IO 事件处理器,它领有 IO 文件句柄 Handle(通过 get\_handle 获取),以及对 Handle 的操作 handle\_event(读 / 写等)。继承于 EventHandler 的子类能够对事件处理器的行为进行定制。Reactor 类用于治理 EventHandler(注册、删除等),并应用 handle\_events 实现事件循环,一直调用同步事件多路分离器(个别是内核)的多路拆散函数 select,只有某个文件句柄被激活(可读 / 写等),select 就返回(阻塞),handle\_events 就会调用与文件句柄关联的事件处理器的 handle\_event 进行相干操作。

图 5 IO 多路复用

如图 5 所示,通过 Reactor 的形式,能够将用户线程轮询 IO 操作状态的工作对立交给 handle\_events 事件循环进行解决。用户线程注册事件处理器之后能够继续执行做其余的工作(异步),而 Reactor 线程负责调用内核的 select 函数查看 socket 状态。当有 socket 被激活时,则告诉相应的用户线程(或执行用户线程的回调函数),执行 handle\_event 进行数据读取、解决的工作。因为 select 函数是阻塞的,因而多路 IO 复用模型也被称为异步阻塞 IO 模型。留神,这里的所说的阻塞是指 select 函数执行时线程被阻塞,而不是指 socket。个别在应用 IO 多路复用模型时,socket 都是设置为 NONBLOCK 的,不过这并不会产生影响,因为用户发动 IO 申请时,数据曾经达到了,用户线程肯定不会被阻塞。

用户线程应用 IO 多路复用模型的伪代码形容为:

void UserEventHandler::handle_event() 
{if(can_read(socket)) 
    {read(socket, buffer);

        process(buffer);
    
    }

}



{Reactor.register(new UserEventHandler(socket));

}

用户须要重写 EventHandler 的 handle\_event 函数进行读取数据、解决数据的工作,用户线程只须要将本人的 EventHandler 注册到 Reactor 即可。Reactor 中 handle\_events 事件循环的伪代码大抵如下。

Reactor::handle_events() 
{while(1) 
    {sockets = select();

        for(socket in sockets) 
        {get_event_handler(socket).handle_event();}

    }

}

事件循环不断地调用 select 获取被激活的 socket,而后依据获取 socket 对应的 EventHandler,执行器 handle\_event 函数即可。

IO 多路复用是最常应用的 IO 模型,然而其异步水平还不够“彻底”,因为它应用了会阻塞线程的 select 零碎调用。因而 IO 多路复用只能称为异步阻塞 IO,而非真正的异步 IO。

四、异步 IO

“真正”的异步 IO 须要操作系统更强的反对。在 IO 多路复用模型中,事件循环将文件句柄的状态事件告诉给用户线程,由用户线程自行读取数据、解决数据。而在异步 IO 模型中,当用户线程收到告诉时,数据曾经被内核读取结束,并放在了用户线程指定的缓冲区内,内核在 IO 实现后告诉用户线程间接应用即可。

异步 IO 模型应用了 Proactor 设计模式实现了这一机制。

图 6 Proactor 设计模式

如图 6,Proactor 模式和 Reactor 模式在结构上比拟类似,不过在用户(Client)应用形式上差异较大。Reactor 模式中,用户线程通过向 Reactor 对象注册感兴趣的事件监听,而后事件触发时调用事件处理函数。而 Proactor 模式中,用户线程将 AsynchronousOperation(读 / 写等)、Proactor 以及操作实现时的 CompletionHandler 注册到 AsynchronousOperationProcessor。AsynchronousOperationProcessor 应用 Facade 模式提供了一组异步操作 API(读 / 写等)供用户应用,当用户线程调用异步 API 后,便继续执行本人的工作。AsynchronousOperationProcessor 会开启独立的内核线程执行异步操作,实现真正的异步。当异步 IO 操作实现时,AsynchronousOperationProcessor 将用户线程与 AsynchronousOperation 一起注册的 Proactor 和 CompletionHandler 取出,而后将 CompletionHandler 与 IO 操作的后果数据一起转发给 Proactor,Proactor 负责回调每一个异步操作的事件实现处理函数 handle\_event。尽管 Proactor 模式中每个异步操作都能够绑定一个 Proactor 对象,然而个别在操作系统中,Proactor 被实现为 Singleton 模式,以便于集中化散发操作实现事件。

图 7 异步 IO

如图 7 所示,异步 IO 模型中,用户线程间接应用内核提供的异步 IO API 发动 read 申请,且发动后立刻返回,继续执行用户线程代码。不过此时用户线程曾经将调用的 AsynchronousOperation 和 CompletionHandler 注册到内核,而后操作系统开启独立的内核线程去解决 IO 操作。当 read 申请的数据达到时,由内核负责读取 socket 中的数据,并写入用户指定的缓冲区中。最初内核将 read 的数据和用户线程注册的 CompletionHandler 分发给外部 Proactor,Proactor 将 IO 实现的信息告诉给用户线程(个别通过调用用户线程注册的实现事件处理函数),实现异步 IO。

用户线程应用异步 IO 模型的伪代码形容为:

void UserCompletionHandler::handle_event(buffer) 
{process(buffer);

}



{aio_read(socket, new UserCompletionHandler);

}

用户须要重写 CompletionHandler 的 handle\_event 函数进行解决数据的工作,参数 buffer 示意 Proactor 曾经筹备好的数据,用户线程间接调用内核提供的异步 IO API,并将重写的 CompletionHandler 注册即可。

相比于 IO 多路复用模型,异步 IO 并不非常罕用,不少高性能并发服务程序应用 IO 多路复用模型 + 多线程工作解决的架构根本能够满足需要。况且目前操作系统对异步 IO 的反对并非特地欠缺,更多的是采纳 IO 多路复用模型模拟异步 IO 的形式(IO 事件触发时不间接告诉用户线程,而是将数据读写结束后放到用户指定的缓冲区中)。Java7 之后曾经反对了异步 IO,感兴趣的读者能够尝试应用。

关注公众号:java 宝典

正文完
 0