Netty实战七之EventLoop和线程模型
简单地说,线程模型指定了操作系统、编程语言、框架或者应用程序的上下文中的线程管理的关键方面。Netty的线程模型强大但又易用,并且和Netty的一贯宗旨一样,旨在简化你的应用程序代码,同时最大限度地提高性能和可维护性。
1、线程模型概述
线程模型确定了代码的执行方式,由于我们总是必须规避并发执行可能会带来的副作用,所以理解所采用的并发模型(也有单线程的线程模型)的影响很重要。
因为具有多核心或多个CPU的计算机现在已经司空见惯,大多数的现代应用程序都利用了复杂的多线程处理技术以有效地利用系统资源。相比之下,在早期的Java语言中,我们使用多线程处理的主要方式无非是按需创建和启动新的Thread来执行并发的任务单元——一种在高负载下工作得很差的原始方式。Java 5随后引入了Executor API,其线程池通过缓存和重用Thread极大地提高了性能。
基本的线程池化模式可以描述为:
——从池的空闲线程列表中选择一个Thread,并且指派它去运行一个已提交的任务(一个Runnable的说笑呢)
——当任务完成时,将该Thread返回给该列表,使其可被重用。
下图说明了这个模型 虽然池化和重用线程相对简单地为每个任务都创建和销毁线程是一种进步,但是它并不能消除由上下文切换所带来的开销,其将随着线程数量的增加很快变得明显,并且在高负载下愈演愈烈。此外,仅仅由于应用程序的整体复杂性或者并发需求,在项目的声明周期内也可能会出现其他的线程相关的问题。
2、EventLoop接口
运行任务来处理在连接的生命周期内发生的事件是任何网络框架的基本功能。与之相应的编程上的构造通常被称为事件循环——一个Netty使用了Interface io.netty.channel.EventLoop来适配的术语。
以下代码说明了事件循环的基本思想,其中的每个任务都是一个Runnable的实例。
while(!terminated){
//阻塞,直到有事件已经就绪可被运行
List<Runnable> readyEvents = blockUntilEventReady();
for (Runnable ev:readyEvents){
//循环遍历,并处理所有的事件
ev.run();
}
}
Netty的EventLoop是协同设计的一部分,它采用了两个基本的API:并发和网络编程。首先,io.netty.util.concurrent包构建在JDK的java.util.concurrent包上,用来提供线程执行器。其次,io.netty.channel包中的类,为了与Channel的事件进行交互,扩展了这些接口/类。
下图展示了生成的类层次结构 在这个模型中,一个EventLoop将由一个永远都不会改变的Thread驱动,同时任务(Runnable或者Callable)可以直接提交给EventLoop实现,以立即执行或者调度执行。根据配置和可用核心的不同,可能会创建多个EventLoop实例用以优化资源的使用,并且单个EventLoop可能会被指派用于服务多个Channel。
需要注意的是,Netty的EventLoop在继承了ScheduledExecutorService的同时,只定义了一个方法,parent(),用于返回到当前EventLoop实现的实例所属的EventLoopGroup的引用。
事件/任务的执行顺序:事件和任务是以先进先出(FIFO)的顺序执行的,这样可以通过保证字节内容总是按正确的顺序被处理,消除潜在的数据损坏的可能性。
3、Netty4中的I/O和事件处理
由I/O操作触发的事件将流经安装了一个或者多个ChannelHandler的ChannelPipeline。传播这些事件的方法调用可以随后被ChannelHandler所拦截,并且可以按需地处理事件。
事件的性质通常决定了它将被如何处理,他可能将数据从网络栈中传递到你的应用程度中,或者进行逆向操作,或者执行一些截然不同的操作。但是事件的处理逻辑必须足够的通用和灵活,以处理所有可能的用例。因此,在Netty4中,所有I/O操作和事件都由已经被分配给了EventLoop的那个Thread来处理。
4、Netty3中的I/O操作
在以前的版本中所使用的线程模型只保证了入站(之前称为上游)事件会在所谓的I/O线程(对应于Netty4中的EventLoop)中执行。所有的出站(下游)事件都由调用线程处理,其可能是I/O线程也可能是别的线程。开始看起来似乎是好主意,但是已经被发现是有问题的,因为需要在ChannelHandler中对出站事件进行仔细的同步。简而言之,不可能保证多个线程不会再同一时刻尝试访问出站事件。例如,如果你通过在不同的线程中调用Channel.write()方法,针对同一个Channel同时触发出站的事件,就会发生这种情况。
当出站事件触发了入站事件时,将会导致另一个负面影响。当Channel.write()方法导致异常时,需要生成并触发一个exceptionCaught事件。但是在Netty3的模型中,由于这是一个入站事件,需要在调用线程中执行代码,然后将事件移交给I/O线程去执行,然而这将带来额外的上下文切换。
Netty4中所采用的线程模型,通过在同一个线程中处理某个给定的EventLoop中所产生的所有事件,解决了这个问题。这提供了一个更加简单的执行体系架构,并且消除了在多个ChannelHandler中进行同步的需要。
5、JDK的任务调度
你需要调度一个任务以便稍后(延迟)执行或者周期性地执行。例如,你可能想要注册一个在客户端已经连接了5分钟之后触发的任务。一个常见的用例是,发送心跳信息到远程节点,以检查连接是否仍然还活着。如果没有响应,你便知道可以关闭该Channel了。
在Java5之前,任务调度时建立在java.util.Timer类之上,其使用了一个后台Thread,并且具有与标准线程相同的限制。随后,JDK提供了java.util.concurrent包,它定义了interface ScheduledExecutorService。
虽然选择不多,但是这些预置的实现已经足够应对大多数的用例。
以下代码展示了如何使用ScheduledExecutorService来在60秒的延迟之后执行一个任务。
//创建一个其线程池具有10个线程的ScheduledExecutorService
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(10);
//创建一个Runnable,以供调度稍后执行
ScheduledFuture<?> future = executorService.schedule(new Runnable() {
@Override
public void run() {
//该任务要打印的消息
System.out.println("60 seconds later");
}
//调度任务在从现在开始的60秒之后执行
},60, TimeUnit.SECONDS);
...
//一旦调度任务执行完成,就关闭ScheduledExecutorService以释放资源
executorService.shutdown();
虽然ScheduledExecutorService API是直接了当的,当时在高负载下它将带来性能上的负担。
6、使用EventLoop调度任务
ScheduledExecutorService的实现具有局限性,例如,事实上作为线程池管理的一部分,将会有额外的线程创建。如果有大量任务被紧凑地调度,那么这将成为一个瓶颈。Netty通过Channel的EventLoop实现任务调度解决了这一问题。
经过60秒之后,Runnable实例将由分配给Channel的EventLoop执行。如果要调度任务以每隔60秒执行一次,请使用sheduleAtFixedRate()方法,如以下代码。
Channel ch = ...;
ScheduledFuture<?> future = ch.eventLoop().schedule(
//创建一个Runnable以供调度稍后执行
new Runnable() {
@Override
public void run() {
//要执行的代码
System.out.println("60 seconds later");
}
//调度任务在从现在开始的60秒之后执行
},60,TimeUnit.SECONDS
);
如前面提到的,Netty的EventLoop扩展了ScheduledExecutorService,所有它提供了JDK实现可用的所有方法,包括前面的schedule()和scheduleAtFixedRate()方法。所有操作的完整列表可以在ScheduledExecutorService的Javadoc中找到。
要取消或者检查(被调度任务)执行状态,可以使用每个异步操作所返回的ScheduledFuture。
以下代码展示了一个简单的取消操作。
Channel ch = ...;
//调度任务,并获得所返回的ScheduledFuture
ScheduledFuture<?> future = ch.eventLoop().scheduleAtFixedRate(...);
//some other code that runs...
boolean mayInterruptIfRunning = false;
//取消该任务,防止它再次运行
future.cancel(mayInterruptIfRunning);
7、线程管理
Netty线程模型的卓越性能取决于对于当前执行的Thread的身份的确定,也就是说,确定它是否分配给当前Channel以及它的EventLoop的那一线程。
如果调用线程正是支撑EventLoop的线程,那么所提交的代码块将会被执行。否则,EventLoop将调度该任务以便稍后执行,并将它放入到内部队列中。当EventLoop下次处理它的事件时,它会执行队列中的那些任务/事件。这也是任何的Thread是如何与Channel直接交互而无需在ChannelHandler中进行额外同步的。
注意,每个EventLoop都有它自己的任务队列,独立于任何其他的EventLoop。下图展示了EventLoop用于调度任务的执行逻辑。这也是Netty线程模型的关键组成部分。 之前已经阐明了不要阻塞当前I/O线程的重要性。我们再以另一种方式重申一次:“永远不要将一个长时间运行的任务放入到执行队列中,因为它将阻塞需要在同一线程上执行的任何其他任务。”如果必须要进行阻塞调用或者执行长时间运行的任务,我们建议使用一个专门的EventExecutor。
除了这种受限的场景,如同传输所采用的不同的事件处理实现一样,所使用的线程模型也可以强烈地影响到排队的任务对整体系统性能的影响。
8、EventLoop线程的分配——异步传输
异步传输实现只使用了少量的EventLoop,而且在当前的线程模型中,它们可能会被多个Channel所共享,这使得可以通过尽可能少量的Thread来支撑大量的Channel,而不是每个Channel分配一个Thread。
下图显示了一个EventLoopGroup,它具有3个固定大小的EventLoop(每个EventLoop都由一个Thread支撑)。在创建EventLoopGroup时就直接分配了EventLoop(以及支撑它们的Thread),以确保在需要时它们是可用的。 EventLoopGroup负责为每个新创建的Channel分配一个EventLoop。在当前实现中,使用顺序循环(round-robin)的方式进行分配以获取一个均衡的分布,并且相同的EventLoop可能会被分配给多个Channel。
一旦一个Channel被分配给一个EventLoop,它将在它的整个生命周期中都是用这个EventLoop,请注意,因为它可以使你从担忧你的ChannelHandler实现中的线程安全和同步问题中解脱出来。
另外,需要注意的是,EventLoop的分配方式对ThreadLocal的使用的影响。因为一个EventLoop通常会被用于支撑多个Channel,所以对于所有相关联的Channel来说,ThreadLocal都将是一样的。这使得它对于实现状态追踪等功能来说是个糟糕的选择。然而,在一些无状态的上下文中,它仍然可以被用于在多个Channel之间共享一些重度的或者代价昂贵的对象,甚至是事件。
9、阻塞传输
用于像OIO(旧的阻塞I/O)这样的其它传输的设计略有不同,如下图所示。 这里每一个Channel都将被分配给一个EventLoop(以及他的Thread)。如果你开发的应用程序使用过java.io包中的阻塞I/O实现,你可能就遇到过这种模型。
但是,正如同之前一样,得到的保证是每个Channel的I/O事件都只会被一个Thread(用于支撑该Channel的EventLoop的那个Thread)处理。这也是另一个Netty设计一致性的例子。

Netty实战七之EventLoop和线程模型的更多相关文章
- Netty学习摘记 —— 再谈EventLoop 和线程模型
本文参考 本篇文章是对<Netty In Action>一书第七章"EventLoop和线程模型"的学习摘记,主要内容为线程模型的概述.事件循环的概念和实现.任务调度和 ...
- 【Netty】EventLoop和线程模型
一.前言 在学习了ChannelHandler和ChannelPipeline的有关细节后,接着学习Netty的EventLoop和线程模型. 二.EventLoop和线程模型 2.1. 线程模型 线 ...
- Netty 框架学习 —— EventLoop 和线程模型
EventLoop 接口 Netty 是基于 Java NIO 的,因此 Channel 也有其生命周期,处理一个连接在其生命周期内发生的事件是所有网络框架的基本功能.通常来说,我们使用一个线程来处理 ...
- 深入Netty逻辑架构,从Reactor线程模型开始
本文是Netty系列第6篇 上一篇文章我们从一个Netty的使用Demo,了解了用Netty构建一个Server服务端应用的基本方式.并且从这个Demo出发,简述了Netty的逻辑架构,并对Chann ...
- 【Netty源码分析】Reactor线程模型
1. 背景 1.1. Java线程模型的演进 1.1.1. 单线程 时间回到十几年前,那时主流的CPU都还是单核(除了商用高性能的小机),CPU的核心频率是机器最重要的指标之一. 在Java领域当时比 ...
- Netty源码分析之Reactor线程模型详解
上一篇文章,分析了Netty服务端启动的初始化过程,今天我们来分析一下Netty中的Reactor线程模型 在分析源码之前,我们先分析,哪些地方用到了EventLoop? NioServerSocke ...
- Netty是什么,Netty为什么速度这么快,线程模型分析
哈喽!大家好,我是小奇,一位热爱分享的程序员 小奇打算以轻松幽默的对话方式来分享一些技术,如果你觉得通过小奇的文章学到了东西,那就给小奇一个赞吧 文章持续更新 一.前言 书接上回,现在下着大雨看来是去 ...
- Netty中的EventLoop和线程模型
一.前言 在学习了ChannelHandler和ChannelPipeline的有关细节后,接着学习Netty的EventLoop和线程模型. 二.EventLoop和线程模型 2.1. 线程模型 线 ...
- 3 - EventLoop和线程模型-事件循环
a). EventLoopGroup为每个新创建的channel分配一个EventLoop,多个channel对应一个EventLoop. b). 一个EventLoop由一个不变的thread驱动, ...
随机推荐
- 可参数化的带优先级的数据选择器的FPGA实现方式探讨
在FPGA设计中,大部分情况下我们都得使用到数据选择器.并且为了设计参数化,可调,通常情况下我们需要一个参数可调的数据选择器,比如M选1,M是可调的参数. 如果,数据选择器是不带优先级的,我们可以使用 ...
- QEMU KVM libvirt 手册(3) - Storage Media
访问Hard Drive 使用-hda –hdb qemu-system-x86_64 -enable-kvm -name ubuntutest -m 2048 -hda ubuntutest.im ...
- [转]Understand QoS at OpenSwitch
danny http://dannykim.me/danny/57771 2014.02.11 14:34:58 (*.193.128.184) 592 >>> Purpose Th ...
- 升讯威微信营销系统开发实践:所见即所得的微官网( 完整开源于 Github)
GitHub:https://github.com/iccb1013/Sheng.WeixinConstruction因为个人精力时间有限,不会再对现有代码进行更新维护,不过微信接口比较稳定,经测试至 ...
- Kali学习笔记11:僵尸扫描案例
什么是僵尸扫描?本质也是端口扫描,不过是一种极其隐蔽的扫描方式 所以几乎不会被发现,不过也有着很大缺陷:扫描条件很高 首先需要有一台僵尸机,这里我找好一台win10僵尸机器,IP地址为:10.14.4 ...
- Docker - 参考信息
初见 从 0 开始了解 Docker 可能是把Docker的概念讲的最清楚的一篇文章 Docker新手指南 8 个基本的 Docker 容器管理命令 Docker 核心技术与实现原理 在线教程 Doc ...
- Python的GUI用法1
代码: #python GUI的例子1 import tkinter as tk class Window: def __init__(self,master): frame = tk.Frame(m ...
- 【emotion】目标初定
现在的我漂浮不定,我的心是凌乱的,虽然在我面前的路有无数条,但是我却不知道哪一条路是属于我的.对于java,我掌握的东西可能并不是系统的,想想也知道,自学一年,能形成什么样子的体系呢?然而在日常的工作 ...
- python高级-包(15)
一.引入包 1.1 有2个模块功能有些联系 receiveMsg.py和sendMsg.py都在msg文件夹里面. 1.2.使用import 文件.模块的方式导入 在桌面创建demo.py文件,并把r ...
- 一文让你彻底理解 Java NIO 核心组件
背景知识 同步.异步.阻塞.非阻塞 首先,这几个概念非常容易搞混淆,但NIO中又有涉及,所以总结一下. 同步:API调用返回时调用者就知道操作的结果如何了(实际读取/写入了多少字节). 异步:相对于同 ...