java nio&netty系列之二reactor模型基础

上一篇章介绍了下NIO的基础,并且也给出了一个简单的代码示例,但是如果想享受NIO带来的高速的快感,就得使用多线程编程了。那么在使用多线程编程之前,有一些关于多线程的东西想分享下:

一、关于多线程

  • 分享点一:在单核CPU上,多线程不一定能比单线程更好。为什么要使用多线程?很多人也许未必去考虑过,我个人认为,使用多线程是为了减少CPU等待的时间,最大化的利用CPU的性能。那为什么说多线程未必比单线程好呢?我简单举个案例。你要扫地和洗马桶,扫地要10分钟,洗马桶要10分钟,你一个人完成这两件事要多少时间?使用单线程工作方式是20分钟,而你如果使用多线程,扫一半地跑过去洗马桶再回过来扫地再回去去洗马桶,那么完成这两件事必定要超过20分钟了。但是如果你要做的事是煮饭和扫地,那么使用多线程就体现出优势了。
  • 分享点二:选择多少个线程能充分利用服务器的性能呢?有很多理论是服务器用的是几核CPU就使用几个线程,这是一个比较通用的配置方式,仅限当你线程中执行的代码无阻塞时。要服务器性能越高,就是等同于让CPU处理业务逻辑的时间越长,所以开几个多线程就是考虑能充分利用这些CPU,但是注意不要过度开多线程,线程间的调度本身会消耗CPU,开越多,调度的代价越高。

      多线程的开发要比单线程复杂很多,所以在进行多线程编程的情况下,要特别的小心。

二、网络模型

      Reactor模式是我在接触netty之后才知道的一个模式,关于这个模式,在Netty实现原理浅析的网络模型里面有详尽的描述,当然作者文章也说,是参考Doug Lea的Scalable IO in Java这位大牛的文章,想要理解这个模型的,可以先去看上述两篇文章,接下去的内容,是基于我对于这些模式的理解的记录,顺便会根据Doug Lea文章的内容稍微翻译摘出来点:

      一个最简单的网络模型拥有的基本模型大概是这样的:

  1. 读取数据
  2. 对数据解码
  3. 处理数据
  4. 对数据编码
  5. 发送数据

      模型一:单线程的reactor模型:

     


       读取数据,对数据解码,处理数据,对数据编码和发送都是在一个线程中的。此时的Reactor可以认为是服务器端的主线程,负责接受客户端的链接,链接建立完毕后将此链接负责分派到一个handler线程去处理剩下的事情。伪代码如下:

       

Java代码   收藏代码
  1. /** 
  2.  * 代码来自Doug Lea 
  3. **/  
  4. Class Server implements Runnable{  
  5.     public void run(){  
  6.         try{  
  7.             ServerSocket ss = new ServerSocket(port);  
  8.             while(!Thread.interrupted()){  
  9.                  new Thread(new Handler(ss.accept())).start();  
  10.             }  
  11.         }  
  12.     }  
  13. }  
  14.   
  15. static class Handler implements Runnable{  
  16.     final Socket socket;  
  17.     Handler(Socket s){   
  18.         socket = s;  
  19.     }  
  20.     public void run(){  
  21.         try{  
  22.             byte[] input = new byte[MAX_INPUT];  
  23.             socket.getInputStream().read(input);  
  24.             byte[] output = process(input);  
  25.             socket.getOutputStream().write(output);  
  26.         }catch(IOException ex){  
  27.              /*...*/  
  28.         }  
  29.     }  
  30. }  

 

       这个模式有什么优缺点?我一个单词一个单词的查询翻译了下Doug Lea的文章,也貌似没明确说,但在Netty原理浅析里面是这样阐述的:

  

       该模型 适用于处理器链中业务处理组件能快速完成的场景。不过,这种单线程模型不能充分利用多核资源,所以实际使用的不多。

       那怎么样才算能充分利用多核CPU资源呢?难道开了多个Handler操作就只是用到了一个CPU核么?想想也不可能,

       我用下面代码测试:

        

Java代码   收藏代码
  1. public class Test {  
  2.     public static void main(String[] ben){  
  3.         Thread work1 = new Work();  
  4.         work1.start();  
  5.         Thread work2 = new Work();  
  6.         work2.start();  
  7.     }  
  8. }  
  9.   
  10. class Work extends Thread{  
  11.     public void run(){  
  12.         Long sum=0l;  
  13.         int i=0;  
  14.         while(i<100000){  
  15.             sum+=i;  
  16.         }  
  17.         System.out.println("end");  
  18.     }  
  19. }  

    我CPU是双核,两个核立马彪满:

    

     所以说上面的这个模型不能充分利用多核CPU,我是持保留意见滴。那么怎样的情况不能利用多核CPU呢?

 

     假设当前服务器为四核CPU,当前只有一个handler线程,handler线程里面执行的业务处理很庞大,比如像上面我的示例,1到100万相加总和这样的计算,在这种情况下,如果把1到100万相加的handler再拆成四个线程去计算,各计算四分之一的数据,那么就可以充分利用四核CPU了(但是如果执行很快,比如计算1到10,拆了反而更慢,因为CPU还要调度的)。

 

      但是又假设如果当前已经有四个handler线程甚至更多,就算不拆也已经用到四核了。

      但是真的如果handler的业务逻辑执行很慢(非阻塞,计算量大的情况),要不要拆呢,因为在服务器端我相信同一个时间用户的访问并发应该会超过CPU的核数?所以个人认为不到需要极端优化性能的前提下还是没必要去拆,太复杂。

      所以说,该模型适用于能快速执行的业务逻辑的系统或者多核CPU系统,在某种场景或者程度上是可以这么理解。

 

 

      题外话,假设handler线程里面出现堵塞,会不会让CPU处于空闲呢?回答这个问题钱这,我又回去翻阅了Think in java中线程的一章,首先看线程的四个状态:

 

  1. 新(New):线程对象已经创建,但尚未启动,所以不可运行
  2. 可运行(Runnable):意味着一旦时间分片机制有空闲的CPU周期提供给一个线程,那这个线程便可立即运行。因此线程可能在、也可能不在运行当众,但一旦条件许可,没有什么能阻止它的运行------它既没有“死”掉,也未被"堵塞"。

  3.  

     

    死(Dead):从自己的run()方法中返回后,一个线程便已经"死"掉。亦可调用stop()令其死掉。.

     

     

  4. 堵塞(Blocked):线程可以运行,但有种东西阻碍了它。若线程处于堵塞状态,调度机制可以简单的跳过它,不给他分配任何CPU时间。除非线程再次进入"可运行"状态,否则不会采取任何操作

      那么何为线程堵塞:

 

  1. 调用sleep(毫秒数),使线程进入"睡眠"状态。在规定的时间内,这个线程是不会运行的。

  2. 用suspend()暂定了线程的执行。除非线程收到resume()消息,否则不会返回"可运行"状态。

  3. 用wait()暂停了线程的执行。除非线程收到notify()或者notifyAll()消息,否则不可变成"可运行".
  4. 线程正在等候一些IO(输入输出)操作完成。
  5. 线程实体调用另一个对象的"同步"方法,但那个对象处于锁定状态,暂时无法使用.

      所以如果handler线程里面有堵塞操作,那么就多开几个线程让CPU去切换执行即可,至于开多少个合适,看堵塞的时间而定。这个淘宝内部有公式的,但是堵塞时间不一定,所以还是需要通过TPS测试才能确定.

 

      模型二:多线程处理的Reactor模式

      所以说上面那个模型也不能说不好,继续跟着Doug Lea的文章继续前行了。Doug Lea提出了几点模型可扩展性的目标(Scalability Goals).

 

 

  • 优雅的流量降级(当客户端连接持续增大到很多时,能避免系统被压垮)
  • 能够持续的控制资源的使用增长(CPU,memory,disk,bandwidth)
  • 能满足一些性能方面的目标,包括低延迟,满足高峰需求,服务质量的保证。
  • 分而自治(Divide-and-conquer)通常是最好的方式.其实就是模块化吧。

    Doug Lea首先介绍了Divide-and-conquer,其核心是将任务划分成小任务来处理,并且是非堵塞的小任务.我猜应该是支持了我上述的理论,将较大的任务分拆成小任务来处理,可能可以更好的利用多核CPU的性能,所以才有了下面这个模型:

    

 

      这个模型把原来handler(这个大handler也可叫worker线程)中的对数据解码,数据处理,对数据编码抽离出来行程一个小handler,用过netty的都知道,netty可以添加多个handler并顺序执行,不知道大家有没有思考过,handler是处理业务逻辑的,我为什么要写多个handler?我一个handler就可以写完。如果放到这个模型里面来,就可以理解了,netty希望我们自己将大的handler分解成一个一个小的handler进行执行,只不过默认情况下这些handler都是在一个线程里面顺序执行(其实就跟一个handler是一样的概念,所以默认情况下netty在worker的选择上是模型一)的,或者更多默认情况下,我们只写了一个handler而已。关于此模型,暂时认为讲到这里即可,暂时还不需要深入.

    

       模型三:多Reator线程的Reator模式 (好怪的名字,将就下)

      上述两个模型都有一个通病,Reactor的Acceptor中,首先是建立链接,然后再把链接分配给指定的handler线程去执行,那么我们知道,建立链接是个阻塞动作,操作很慢,那么Doug Lea为了匹配CPU和IO的速率(Use to match CPU and IO rates)提出了下面这个模型:

       

 

 

       Doug Lea的所谓匹配IO和CPU的速率,其实所得直白点是因为只有一个Reator情况下,因为Reator只能一个一个处理创建连接这个请求,如果客户端连接数量一大,就会在Reator形成一个单线程操作,并且创建链接又是一个阻塞动作,只有前一个客户的链接创建完毕才会处理下一个客户的链接请求,性能直线下降。所以这个模型就有了mainReator和subReactor这两个reactor。mainReator负责将用户请求的链接提交给subReactor去执行,subReactor则负责建立链接,并将建立后的链接发送到具体的work线程去执行.

       下一篇章:会先写netty所使用的reactor模型与其根据netty源码简化后的代码。.

 

 

 

 

 

 

 

 

 

 

        

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值