一、异步任务调度
如果 Handler处理器有一些比较长时间的耗时业务处理等操作时,我们可以交给任务队列异步处理。
1、任务队列如何使用
- 自定义任务 : 自己开发的任务 , 然后将该任务提交到任务队列中 ;
- 自定义定时延时任务 : 自己开发的任务 , 然后将该任务提交到任务队列中 , 同时可以指定任务的延时执行时间 ;
- 其它线程调度任务 : 上面的任务都是在当前的 NioEventLoop ( 反应器 Reactor 线程 ) 中的任务队列中排队执行 , 在其它线程中也可以调度本线程的 Channel 通道与该线程对应的客户端进行数据读写 ;
2、Handler处理器同步异步
在前面的 Netty 服务器与客户端的 Demo中 , 我们自定义的 Handler 处理器都是同步操作。
该处理器继承 ChannelInboundHandlerAdapter 类重写的 channelRead方法,或者继承它的子类 SimpleChannelInboundHandler类重写 channelRead0方法。
然后重写的执行业务逻辑时要注意以下两点 :
- 同步操作 : 如果在该业务逻辑中只执行一个短时间的操作 , 那么可以直接执行 ;
- 异步操作 : 如果在该业务逻辑中执行访问数据库 , 访问网络 , 读写本地文件 , 执行一系列复杂计算等耗时操作 , 肯定不能在该方法中处理 , 这样会阻塞整个线程 ; 正确的做法是将耗时的操作放入任务队列中 , 异步执行 ;
在 ChannelInboundHandlerAdapter 的 channelRead 方法执行时 , 客户端与服务器端的反应器 Reactor 线程 NioEventLoop 是处于阻塞状态的 , 此时服务器端与客户端同时都处于阻塞状态 , 这样肯定不行 , 因为 NioEventLoop 需要为多个客户端服务 , 不能因为与单一客户端交互而产生阻塞 ;
3、自定义任务的执行顺序
-
多任务执行 :
如果用户连续向任务队列中放入了多个任务 , NioEventLoop 会按照顺序先后执行这些任务 , 注意任务队列中的任务是先后执行 , 不是同时执行 ; -
先后顺序执行任务 ( 不是并发 ) :
任务队列任务执行机制是顺序执行的 ; 先执行第一个 , 执行完毕后 , 从任务队列中获取第二个任务 , 执行完毕之后 , 依次从任务队列中取出任务执行 , 前一个任务执行完毕后 , 才从任务队列中取出下一个任务执行 ;
4、自定义 taskQueue任务队列
4.1 自定义任务流程 :
- 获取通道 : 首先获取通道 Channel ;
- 获取线程 : 获取通道对应的 EventLoop 线程 , 就是 NioEventLoop , 该 NioEventLoop 中封装了任务队列 TaskQueue ;
- 任务入队 : 向任务队列 TaskQueue 中放入异步任务 Runnable , 调用 NioEventLoop 线程的
execute 方法
, 即可将上述 Runnable 异步任务放入任务队列TaskQueue
;
4.2 代码示例 :
public class MyServerHandler extends ChannelInboundHandlerAdapter {
private static final Logger log = LoggerFactory.getLogger(MyServerHandler.class);
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("MyServerHandler 连接已建立...");
super.channelActive(ctx);
}
/**
* 读取数据 : 在服务器端读取客户端发送的数据
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 获取客户端发送过来的消息
ByteBuf in = (ByteBuf) msg;
log.info("Server Accept Client Context ("+ ctx.channel().remoteAddress() +")消息 ->" + in.toString(CharsetUtil.UTF_8));
// 1 . 从 ChannelHandlerContext中获取通道
Channel channel = ctx.channel();
// 2 . 获取通道对应的事件循环
EventLoop eventLoop = channel.eventLoop();
// 3 . 在 Runnable 中用户自定义耗时操作, 异步执行该操作, 该操作不能阻塞在此处执行
eventLoop.execute(new Runnable() {
@Override
public void run() {
//TODO 执行耗时操作
try {
TimeUnit.SECONDS.sleep(10);
log.info("异步任务 1执行,Thread = {}", Thread.currentThread().getName());
//请求讲当前的msg 通过ChannelPipeline 写入数据到目标Channel 中。
// 值得注意的是:write 操作只是将消息存入到消息发送环形数组中,并没有真正被发送,只有调用flush 操作才会被写入到Channel 中,发送给对方。
ctx.writeAndFlush(Unpooled.copiedBuffer("异步任务 1", CharsetUtil.UTF_8));
log.info("异步任务 1执行完毕");
} catch (Exception ex) {
System.out.println("发生异常" + ex.getMessage());
}
}
});
ctx.channel().eventLoop().execute(new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(5);
log.info("异步任务 2执行,Thread = {}" , Thread.currentThread().getName());
ctx.writeAndFlush(Unpooled.copiedBuffer("异步任务 2", CharsetUtil.UTF_8));
log.info("异步任务 2执行完毕");
} catch (Exception ex) {
log.info("发生异常,message={}" + ex.getMessage());
}
}
});
log.info("channelReadComplete方法执行完毕,Thread = {}", Thread.currentThread().getName());
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
// 发送消息给客户端
ByteBuf byteBuf = Unpooled.copiedBuffer("Server Received Client msg.", CharsetUtil.UTF_8);
ctx.writeAndFlush(byteBuf);
log.info("channelReadComplete方法执行完毕,Thread = {}", Thread.currentThread().getName());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
// 发生异常,关闭通道
// cause.printStackTrace();
ctx.close();
}
}
先启动服务端,再启动客户端,结果如下:
MyServer打印结果:
MyClient打印结果:
从打印结果,可以看出:
- 用户连续向任务队列中放入了多个任务时 NioEventLoop 会按照顺序先后执行这些任务。
- 操作IO线程,使用的都是同一个线程。
5、自定义 scheduleTaskQueue延时任务队列
scheduleTaskQueue延时任务队列和 taskQueue任务队列非常相似。
5.1 自定义延时任务流程 :
- 获取通道 : 首先获取 通道 Channel ;
- 获取线程 : 获取通道对应的 EventLoop 线程 , 就是 NioEventLoop , 该 NioEventLoop 中封装了任务队列 TaskQueue ;
- 任务入队 : 向任务队列 ScheduleTaskQueue 中放入异步任务 Runnable , 调用 NioEventLoop 线程的
schedule 方法
, 即可将上述 Runnable 异步任务放入任务队列ScheduleTaskQueue
;
5.2 代码示例 :
先启动服务端,再启动客户端,结果如下:
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 获取客户端发送过来的消息
ByteBuf in = (ByteBuf) msg;
log.info("Server Accept Client Context (" + ctx.channel().remoteAddress() + ")消息 ->" + in.toString(CharsetUtil.UTF_8));
ctx.channel().eventLoop().schedule(() -> {
try {
// TODO 执行耗时操作
log.info("延迟异步任务执行,Thread = {}", Thread.currentThread().getName());
TimeUnit.SECONDS.sleep(5);
ctx.writeAndFlush(Unpooled.copiedBuffer("异步任务 2", CharsetUtil.UTF_8));
log.info("延迟异步任务执行完毕");
} catch (Exception ex) {
log.info("发生异常,message={}" + ex.getMessage());
}
}, 5, TimeUnit.SECONDS);
log.info("channelRead方法执行完毕,Thread = {}", Thread.currentThread().getName());
}
MyServer打印结果:
从打印结果,可以看出:任务延迟了 5s之后才被执行。
6、自定义任务和自定义定时任务区别
自定义任务和自定义定时任务区别流程基本类似 , 有以下两个不同之处:
(1)调度⽅法 :
- 定时异步任务使⽤ schedule ⽅法进⾏调度
- 普通异步任务使⽤ execute ⽅法进⾏调度
(2)任务队列 :
-
定时异步任务提交到 ScheduleTaskQueue 任务队列中 ;
-
普通异步任务提交到 TaskQueue 任务队列中 ;
二、异步线程池
上面使用异步任务调度所使用的的线程仍然是和IO操作是同一个线程,因此如果做的是比较耗时的工作或不可预料的操作,⽐如数据库,⽹络请求,会严重影响 Netty 对 Socket 的处理速度。
1、handler 中加入线程池
解决方法:我们可以将耗时任务添加到异步线程池中。
使用方法比较简单,就是创建了一个EventExecutorGroup,并向其中提交任务。
可以看到,这样就是用了与IO操作不同的线程来处理业务逻辑。并且每个客户端的请求使用的都是不同的线程。
public class MyServerHandler3 extends ChannelInboundHandlerAdapter {
private static final Logger log = LoggerFactory.getLogger(MyServerHandler3.class);
private static final EventExecutorGroup eventExecutorGroup = new DefaultEventExecutorGroup(3);
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("MyServerHandler 连接已建立...");
super.channelActive(ctx);
}
/**
* 读取数据 : 在服务器端读取客户端发送的数据
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 获取客户端发送过来的消息
ByteBuf in = (ByteBuf) msg;
log.info("Server Accept Client Context (" + ctx.channel().remoteAddress() + ")消息 ->" + in.toString(CharsetUtil.UTF_8));
for (int i = 1; i <= 5; i++) {
int finalI = i;
eventExecutorGroup.submit(() -> {
//TODO 执行耗时操作
try {
TimeUnit.SECONDS.sleep(10);
log.info("异步任务 {}执行,Thread = {}", finalI, Thread.currentThread().getName());
ctx.writeAndFlush(Unpooled.copiedBuffer("异步任务 " + finalI, CharsetUtil.UTF_8));
log.info("异步任务 {}执行完毕", finalI);
} catch (Exception ex) {
System.out.println("发生异常" + ex.getMessage());
}
});
}
log.info("channelRead方法执行完毕,Thread = {}", Thread.currentThread().getName());
}
...
}
– 求知若饥,虚心若愚。