随笔分类
EventLoop
设计及其线程模型
事件循环
EventLoop定义了 Netty的核心抽象,用户处理连接的生命周期中所发生的的事件
- 一个 EventLoopGroup包含一个或者多个 EventLoop
- 一个 EventLoop在它的生命周期内之和一个 Thread绑定
- 所有由 EventLoop处理的 IO事件都在它专有的 Thread上被处理
- 一个 Channel生命周期内只注册到一个 EventLoop中
- 一个 EventLoop可能会被分配给一个或多个 Channel
在这种设计中,一个给定的 Channel的 IO操作始终是由同一个 Thread来去执行,这实际上也 消除了对于同步的需要
本质上是一个 单线程执行器 (里面维护了一个 Selector)
,里面有 run方法处理 Channel上源源不断的 IO事件
EventLoop的继承关系图比较复杂:
-
一条线:EventLoop继承了 juc中的
ScheduledExecutorService
,因此其包含了线程池中所有的方法每个事件循环对象其实就是一个
单线程版的线程池
-
另一条:EventLoop继承了 Netty自己的
OrderedEventExecutor
提供了 boolean isEventLoop(Thread thread)方法判断一个线程是否属于此 EventLoop
提供了 parent方法来看看自己是属于那个 EventLoopGroup
一般不会单独去使用某个 EventLoop
事件 / 任务的执行顺序
事件和任务是以
先进先出
(FIFO)的顺序执行的;这样可以通过保证字节内容总是按正确的顺序被处理,消除潜在的数据损坏的可能性
在 Netty 4中,所有的 IO操作和事件都由已经被分配给了那个 Thread来进行处理
这和 Netty 3中的线程模型是不一样的:Netty 3中保证了上游的事件会在 IO线程中执行,而下游的事件会去由调用线程来进行处理,有可能是 IO线程或者其它线程;但这样问题就来了,因为 ChannelHandler需要对 下游的事件进行仔细的同步
,而我们并不能保证不会有多个线程在同一尝试去访问下游事件:如,多个线程同时去调用了 Channel.write()
,针对同一个 Channel同时触发下游事件,就会发生这种情况.
因此,这也是 Netty 3的线程模型问题所在,so,在 Netty 4中所采用的线程模型,通过在同一个线程中处理某个给定的 EventLoop上所产生的所有事件,解决了这个问题:单线程的线程池,这不就解决了在多个 ChannelHandler中进行同步的需要?
线程管理
Netty线程模型的卓越性能取决于对于执行的 Thread的身份的确定 (通过调用 inEventLoop(thread)来实现),即确认调用线程是否是分配给当前 Channel以及它的 EventLoop的那一个线程
如果 (当前)调用线程正是支撑 EventLoop的线程,那么所提交的任务将会被立即执行 (定时任务不是...);否则,EventLoop将会调度该任务以便稍后执行,并将它放入到 内部队列中
;当 EventLoop下次处理它的事件时,它会执行队列中的那些任务 / 事件;
这也解释了任何 Thread是如何与 Channel直接交互而无需在 ChannelHandler中进行额外同步
但需始终铭记一点:
永远不要去将一个长时间运行的任务放入到执行队列中,因为它将阻塞在同一线程上执行的任何其它任务
.
ThreadLocal & EventLoop?
由于 Netty的设计,一个 EventLoop可能会去关联多个 Channel
so,我们便需要去关注下,EventLoop的分配方式对 ThreadLocal的使用的影响
对于 EventLoop上所关联的 Channels而言,它们所看到的的 ThreadLocal都将会是一样的,这使得它对于实现状态追踪等功能来说是个 糟糕的选择
然而,在一些无状态的上下文中,它仍然可以被用于在多个 Channel之间共享一些重度的或者代价昂贵的对象,甚至是事件
EventLoopGroup
一组 EventLoop,Channel一般会调用 EventLoopGroup的 register方法来绑定其中一个 EventLoop,后续这个 Channel上的 IO事件都有此 EventLoop来进行处理 (这也保证了 IO事件处理时的线程安全)
即,一个 EventLoop可以去管理多个 Channel
其实有时候,代码就是最好的注释!
EventLoopServer
// 可以发现 Netty、Nio中大多数接口都是级联, 合理使用真的可以来简化 coding
@Slf4j
public class EventLoopServer {
public static void main(String[] args) {
// 2. 创建一个独立的 EventLoopGroup
// 如果某个 handler的处理逻辑耗时比较长的话, 为了避免 Netty中的线程处理该 handler而去影响了其它 io操作 (多路复用)
// 可以为某个耗时比较久的 handler单独去使用一个 EventLoopGroup去执行它
EventLoopGroup group = new DefaultEventLoopGroup();
new ServerBootstrap()
// 1.如果只有一个 EventLoopGroup的话, 那么里面会有不同的时间循环对象去负责不同的类型的事件 - 即职责划分得不明确
// Netty中建议我们将 EventLoopGroup划分得更细一些:Boss、Worker
// parentGroup - Boss, 只负责 accept事件 childGroup - Worker, 只负责读写事件
// 对于第一个 Group实际上是不需要手动去指定线程数为 1的, 为什么呢?
// 因为服务端这边只有一个, 对应的 ServerSocketChannel只会去绑定该 group中的一个 EventLoop, 即对应也只会有一个线程而已...(个人理解)
.group(new NioEventLoopGroup(), new NioEventLoopGroup(2)) // 底层多路复用, 一个 EventLoop去管理多个 channel
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>(){
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
// 自定义数据处理的 handler
ch.pipeline().addLast("handler1" ,new ChannelInboundHandlerAdapter() {
@Override // ByteBuf
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
// 实际开发时不要去使用默认字符集 - 指定具体的字符集
log.debug(buf.toString(Charset.defaultCharset()));
// 这里需要将消息传递给下一个处理器 - 否则的话消息就会在这个处理器断开
ctx.fireChannelRead(msg);
}
}).addLast(group, "handler2", new ChannelInboundHandlerAdapter() { // 这里指定了一个 group专门去处理此 handler, 即将 handler的执行权交给 group中的某个 EventLoop
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
log.debug(buf.toString(Charset.defaultCharset()));
}
});
}
})
.bind(8080);
}
}