I/O模型归纳

  1. IO

1.广义IO模型

广义12种IO模型

        备注:

        1.IO复用指的是能否让一条线程来处理多个Socket,这个必须是应用了选择器,而不是程序员手工书写。

        2.互通指的是多个客户连接之间是否能方便的交换数据,特指服务端交换,而不是客户端交换。

        3.顺序性是指如果客户端按照顺序发送多个请求,那么计算得到的多个响应是否按照相同的顺序发还给客户端。

        4.线程数指的是在程序跑起来之后,线程数是否恒定不变。

       

           方案0:

服务端一个进程:

最原始的单进程模型,适合做短连接,在服务端就一个进程来服务。请求必须一个一个处理,如果请求很多,那就只能让后来的请求阻塞。

优点:性能消耗低,可以保证顺序

缺点:效率不高

方案1:

服务端是多进程:

服务端主进程会为每一个请求新建一个子进程来服务,这种模型适合并发数不大、长连接,不适合短连接。因为每一次新建进程都会调用fork()函数,而如果fork()函数的开销比后台处理的时间还长,那就得不偿失了。

优点:可以保证顺序,适合长连接

缺点:进程太多导致开销太大

方案2:

单进程内多线程:

Java1.4之前的BIO,来一个请求之后,新开一条线程来处理这个请求,这样就可以减少阻塞。新建线程的开销比新建进程的开销要小很多,但是仍然不适合短连接。这种方案依然受到线程数量的限制,一两百个线程还可以,几千个线程对操作系统来讲仍然是一个不小的压力。

优点:相比方案1性能开销变小了

缺点:线程数量仍然受限制

方案3:

    提前创建多进程:    

针对方案1的的优化,预先申请多个进程,这是apache httpd作为反向代理服务器的实现方式。

优点:省却了创建进程的时间,提高了响应效率

缺点:依然有大量进程,性能开销较大

 

方案4:

提前创建多线程:

针对方案2的优化,预先申请多条线程,这同样是apache httpd作为反向代理服务器的实现方式。

优点:省却了创建线程的时间

缺点:依然有大量的线程,性能开销较大

   

以上都是阻塞式网络编程,也就是说当一个进程或者线程阻塞在read()方法上,但是程序又想给这个TCP连接发数据,就没有办法发了。所以就出现了是select/poll/epoll/kqueue这一系列的多路选择器。让一条线程可以处理多个连接,”IO复用”其实复用的不是IO连接,而是复用了线程,他是把线程的数量减少了。感谢Doug Schmidt为我们总结出了Reactor 模式,让event-driven 网络编程有章可循。继而出现了一些通用的Reactor框架/库,比如libeventmuduoNettytwistedPOE等等。有了这些库,我想基本不用去编写阻塞式的网络程序了。

Reactor模式,也就是事件驱动模式,他的意义就在于将IO事件分发到用户提供的处理函数,并保持网络部分的通用代码不变,独立于用户的业务逻辑,其实说白了,就是Linux本身提供一些多路选择器的函数,用户空间就只负责调用就好了,只关注业务层面就好了。

 

方案5:

IO线程+多路选择器:

多路选择器模型,Java1.4之后有了NIO,将NIO包装起来的框架有Netty,他们的底层都是用的Linux的函数。

 

 

 

 

 

 

 

 

 

 


     一条IO线程+多路选择器=单线程Reactor模型,执行顺序如下图。在没有事件的时候,线程等待在select/poll/epoll_wait 等函数上。事件到达后由网络库处理IO,再把消息通知(回调)客户端代码。Reactor事件循环所在的线程通常叫IO 线程。通常由网络库负责读写socket,用户代码负载解码、计算、编码。注意由于只有一个线程,因此事件是顺序处理的,一个线程同时只能做一件事情。在协作式多任务中,事件的优先级得不到保证。

优点:

由网络库搞定数据收发,程序只关心业务逻辑

缺点:

适合IO密集型应用,不适合CPU密集,因为单线程,很难发挥多核的威力;此外与方案2相比,本方案处理网络消息的延迟可能要略大一些,因为方案2直接一次read()系统调用就可以请求到数据,而方案5要先poll轮询到这个Socket,然后再针对这个Socket做一次read()调用,多了一次系统调用。

 

方案6:

     IO线程+多路选择器+临时创建任务计算线程:

     因为如果计算量很大,就会影响效率,所以尽量不要在IO线程内计算。可以将每一个请求(不是连接)都创建一个线程。这是一个临时方案。

     优点:

     提高了计算效率,因为每一个请求都申请一个线程

     缺点:

     线程容易多,导致开销很大,而且没有顺序性

 

方案7:

     IO线程+多路选择器+创建多个连接计算线程:

     方案6是每一个请求都创建一条线程,现在是每一个连接创建一条线程,同一个连接的相关请求都发送到同一条计算线程上去。这样的话,就可以减少很多线程。这是一个临时方案。这个方案有的时候还不如方案2,因为方案2简单,这个太麻烦了。

     优点:

     相比方案6可以减少很多线程。

     缺点:

     由于依然是一条连接对应一条线程,其实总数上看,线程数还是挺多的。

方案8:

     IO线程+多路选择器+任务计算线程池:

为了弥补方案6中每个请求创建线程的缺陷,我们使用固定大小线程池。IO工作依然放到IO线程池中去完成,而计算的任务交给thread pool。如果计算任务彼此独立,而且IO压力不大,例如数据库的设计,这种方案非常合适。

方案8使用线程池与单线程Reactor的方案5相比变化不大,只是把计算和返回响应的部分抽出来做成一个函数,然后交给线程池了。本方案有乱序的可能,客户端要根据ID来匹配响应。

优点:

线程的数量固定下来了,不会对系统有较高的负载;且可以将一些阻塞操作放到线程池,避免IO线程被阻塞了。

缺点:

仅仅只有一个IO线程,所以只能适用于IO压力不大的场景,而压力较大的场景是不适用的。

 

方案9:

单一主IO线程+多个子IO线程+多路选择器

主线程负责accept连接,然后把连接送到某一个子线程上,所有的计算部分都放到子线程。这是Netty的实现模型。

 优点:

 主线程只需要连接,不用执行read()和计算等操作

 缺点:

 IO线程依然负责了计算部分。

方案10:

 IO进程+多个子IO进程+多路选择器

 这是Nginx的模型,各个进程之间互相独立,还可以将进程做CPU绑定。

 优点:

 主进程和子进程模式隔离。

 缺点:

 IO线程依然负责了计算部分。

 

方案11:

     IO线程+多个子IO线程+多路选择器+线程池

     IO线程负责监听请求,将请求交给子IO线程,子IO线程利用多路选择器来管理多个连接。并将计算部分抽离到线程池。

     该方案几乎完美。这也是方案9的改进型。

 



猜你喜欢

转载自blog.csdn.net/sqsfjsjlpf/article/details/71326378