前面两篇文章提到了 BossGroup 和 WorkerGroup ,它们的类型是 NioEventLoopGroup,NioEventLoopGroup 相当于一个 事件循环组,这个组中 含有多个事件循环 ,每一个事件循环是 NioEventLoop,而NioEventLoop 中有两个重要属性:Selector
和 TaskQueue
。那么这个 TaskQueue,即任务队列,有什么作用呢?
我们前面分析了在 事件循环(NioEventLoop)
的过程中,我们会在 pipline 中调用 Handler 来处理我们的业务,那么假如在某一个 Handler有一个长时间的操作,这就势必会造成 pipiline 的阻塞,这是我们就可以将这个耗时的处理提交到 TaskQueue 进行异步的执行。
1 TaskQueue 的3种典型使用场景
- 场景一:用户程序自定义的普通任务
- 场景二:用户自定义定时任务
- 场景三:非当前 Reactor 线程调用 Channel 的各种方法。
- 例如在
推送系统
的业务线程里面,根据用户的标识
,找到对应的Channel 引用
,然后调用 Write 类方法向该用户推送消息,就会进入到这种场景。最终的 Write 会提交到任务队列中后被异步消费
。
- 例如在
2 代码演示
2.1 问题分析
首先我们 在 NettyServerHandler 的 channelRead() 方法中模拟一下耗时的业务:
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 比如这里我们有一个非常耗时的业务,我们想让它异步的执行
// 只需要将它提交到该 channel 对应的 NioEventLoop 的 TaskQueue 中
Thread.sleep(5 * 1000);// 模拟耗时业务
// 执行完业务向客户端返回消息
ctx.writeAndFlush(Unpooled.copiedBuffer("耗时业务执行完毕...",CharsetUtil.UTF_8));
System.out.println("go on...");
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
// writeAndFlush 是 write + flush
// 将数据写入到缓存,并刷新
// 一般我们会对这个发送的数据进行编码
ctx.writeAndFlush(Unpooled.copiedBuffer("hello,客户端...",CharsetUtil.UTF_8));
}
}
此时客户端必然是等待 5秒 后才能收到消息:耗时业务执行完毕…;然后再收到消息:hello,客户端…。
2.2 解决方案一:用户程序自定义的普通任务
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 比如这里我们有一个非常耗时的业务,我们想让它异步的执行
// 只需要将它提交到该 channel 对应的 NioEventLoop 的 TaskQueue 中
// 解决方案1:用户程序自定义的普通程序
ctx.channel().eventLoop().execute(new Runnable() {
public void run() {
try {
Thread.sleep(5 * 1000);// 模拟耗时业务
// 执行完业务向客户端返回消息
ctx.writeAndFlush(Unpooled.copiedBuffer("耗时业务执行完毕...",CharsetUtil.UTF_8));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
System.out.println("go on...");
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
// writeAndFlush 是 write + flush
// 将数据写入到缓存,并刷新
// 一般我们会对这个发送的数据进行编码
ctx.writeAndFlush(Unpooled.copiedBuffer("hello,客户端...",CharsetUtil.UTF_8));
}
这是我们启动程序发现:首先收到消息:hello,客户端…;然后过 5秒 收到消息:耗时业务执行完毕…
验证也很简单,Debug查看:ctx ==》 pipline ==》channel 》eventLoop》taskQueue,看一下 taskQueue 的size就行了。
2.3 解决方案二:用户自定义定时任务
注意:这种解决方案任务是提交到 scheduleTaskQueue 中。
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 比如这里我们有一个非常耗时的业务,我们想让它异步的执行
// 只需要将它提交到该 channel 对应的 NioEventLoop 的 TaskQueue 中
// 解决方案2:用户自定义定时任务,该任务提交到 scheduleTaskQueue 中
ctx.channel().eventLoop().schedule(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5 * 1000);// 模拟耗时业务
// 执行完业务向客户端返回消息
ctx.writeAndFlush(Unpooled.copiedBuffer("耗时业务执行完毕...",CharsetUtil.UTF_8));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},5, TimeUnit.SECONDS);
System.out.println("go on...");
}
验证也很简单,Debug查看:ctx ==》 pipline ==》channel 》eventLoop》scheduleTaskQueue,看一下 scheduleTaskQueue 的 size 就行了。
3 方案再说明
- Netty 抽象出两组线程池,BossGroup 专门负责接收客户端连接,WorkerGroup 专门负责网络读写操作。
- NioEventLoop 表示一个不断循环执行处理任务的线程,每个 NioEventLoop 都有一个selector,用于监听绑定在其上的 socket 网络通道。
- NioEventLoop 内部采用串行化设计,从消息的
读取 ==》解码 ==》处理 ==》编码 ==》发送
,始终由IO 线程 NioEventLoop 负责 - NioEventLoopGroup 下包含多个 NioEventLoop,每个 NioEventLoop 中包含有一个 Selector,一个 taskQueue,每个 NioEventLoop 的 Selector 上可以注册监听多个 NioChannel,每个 NioChannel 只会绑定在唯一的 NioEventLoop 上,每个 NioChannel 都绑定有一个自己的 ChannelPipeline。