15.2 事件循环

    事件循环所做的正如它的名字,它运行的事件在一个循环中,直到循环终止。这非常适合网络框架的设计,因为它们需要为一个特定的连接运行一个事件循环。这不是Netty的新发明,其他的框架和实现已经很早就这样做了。

    在Netty中使用EventLoop接口代表事件循环,EventLoop是从EventExecutor和ScheduledExecutorService扩展而来,所以可以讲任务直接交给EventLoop执行。类关系图如下:

http://img.blog.csdn.net/20140807152444796?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvYWJjX2tleQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast

15.2.1 使用事件循环

    下面代码显示如何访问已分配给通道的EventLoop并在EventLoop中执行任务:

[java] view plaincopy在CODE上查看代码片

  1. Channel ch = ...;
  2. ch.eventLoop().execute(new Runnable() {
  3. @Override
  4. public void run() {
  5. System.out.println("run in the eventloop");
  6. }
  7. });

    使用事件循环的好处是不需要担心同步问题,在同一线程中执行所有其他关联通道的其他事件。这完全符合Netty的线程模型。检查任务是否已执行,使用返回的Future,使用Future可以访问很多不同的操作。下面的代码是检查任务是否执行:
    

[java] view plaincopy在CODE上查看代码片

  1. Channel ch = ...;
  2. Future<?> future = ch.eventLoop().submit(new Runnable() {
  3. @Override
  4. public void run() {
  5. }
  6. });
  7. if(future.isDone()){
  8. System.out.println("task complete");
  9. }else {
  10. System.out.println("task not complete");
  11. }

    检查执行任务是否在事件循环中:

[java] view plaincopy在CODE上查看代码片

  1. Channel ch = ...;
  2. if(ch.eventLoop().inEventLoop()){
  3. System.out.println("in the EventLoop");
  4. }else {
  5. System.out.println("outside the EventLoop");
  6. }

    只有确认没有其他EventLoop使用线程池了才能关闭线程池,否则可能会产生未定义的副作用。
    

15.2.2 Netty4中的I/O操作

    这个实现很强大,甚至Netty使用它来处理底层I/O事件,在socket上触发读和写操作。这些读和写操作是网络API的一部分,通过java和底层操作系统提供。下图显示在EventLoop上下文中执行入站和出站操作,如果执行线程绑定到EventLoop,操作会直接执行;如果不是,该线程将排队执行:

http://img.blog.csdn.net/20140807152800186?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvYWJjX2tleQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast

    需要一次处理一个事件取决于事件的性质,通常从网络堆栈读取或传输数据到你的应用程序,有时在另外的方向做同样的事情,例如从你的应用程序传输数据到网络堆栈再发送到远程对等通道,但不限于这种类型的事物;更重要的是使用的逻辑是通用的,灵活处理各种各样的案例。

    应该指出的是,线程模型(事件循环的顶部)描述并不总是由Netty使用。我们在了解Netty3后会更容易理解为什么新的线程模型是可取的。

15.2.3 Netty3中的I/O操作

    在以前的版本有点不同,Netty保证在I/O线程中只有入站事件才被执行,所有的出站时间被调用线程处理。这看起来是个好方案,但很容易出错。它还将负责同步ChannelHandler来处理这些事件,因为它不保证只有一个线程同时操作;这可能发生在你去掉通道下游事件的同时,例如,在不同的线程调用Channel.write(...)。下图显示Netty3的执行流程:

http://img.blog.csdn.net/20140807152709500?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvYWJjX2tleQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast

    除了需要负担同步ChannelHandler,这个线程模型的另一个问题是你可能需要去掉一个入站事件作为一个出站事件的结果,例如Channel.write(...)操作导致异常。在这种情况下,捕获的异常必须生成并抛出去。乍看之下这不像是一个问题,但我们知道,捕获异常由入站事件涉及,会让你知道问题出在哪里。问题是,事实上,你现在的情况是在调用线程上执行,但捕获到异常事件必须交给工作线程来执行。这是可行的,但如果你忘了传递过去,它会导致线程模型失效;假设入站事件只有一个线程不是真,这可能会给你各种各样的竞争条件。

    以前的实现有一个唯一的积极影响,在某些情况下它可以提供更好的延迟;成本是值得的,因为它消除了复杂性。实际上,在大多数应用程序中,你不会遵守任何差异延迟,还取决于其他因数,如:
  • 字节写入到远程对等通道有多快
  • I/O线程是否繁忙
  • 上下文切换
  • 锁定

你可以看到很多细节影响整体延迟。

15.2.4 Netty线程模型内部

    Netty的内部实现使其线程模型表现优异,它会检查正在执行的线程是否是已分配给实际通道(和EventLoop),在Channel的生命周期内,EventLoop负责处理所有的事件。如果线程是相同的EventLoop中的一个,讨论的代码块被执行;如果线程不同,它安排一个任务并在一个内部队列后执行。通常是通过EventLoop的Channel只执行一次下一个事件,这允许直接从任何线程与通道交互,同时还确保所有的ChannelHandler是线程安全,不需要担心并发访问问题。

    下图显示在EventLoop中调度任务执行逻辑,这适合Netty的线程模型:

http://img.blog.csdn.net/20140807152914156?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvYWJjX2tleQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast

    设计是非常重要的,以确保不要把任何长时间运行的任务放在执行队列中,因为长时间运行的任务会阻止其他在相同线程上执行的任务。这多少会影响整个系统依赖于EventLoop实现用于特殊传输的实现。传输之间的切换在你的代码库中可能没有任何改变,重要的是:切勿阻塞I/O线程。如果你必须做阻塞调用(或执行需要长时间才能完成的任务),使用EventExecutor。

    下一节将讲解一个在应用程序中经常使用的功能,就是调度执行任务(定期执行)。Java对这个需求提供了解决方案,但Netty提供了几个更好的方案。

results matching ""

    No results matching ""