Netty 异步任务调度与异步线程池

一、异步任务调度

如果 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打印结果:
Netty 异步任务调度与异步线程池_第1张图片

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打印结果:
Netty 异步任务调度与异步线程池_第2张图片
从打印结果,可以看出:任务延迟了 5s之后才被执行。

6、自定义任务和自定义定时任务区别

自定义任务和自定义定时任务区别流程基本类似 , 有以下两个不同之处:

(1)调度⽅法 :

  • 定时异步任务使⽤ schedule ⽅法进⾏调度
  • 普通异步任务使⽤ execute ⽅法进⾏调度

(2)任务队列 :

  • 定时异步任务提交到 ScheduleTaskQueue 任务队列中 ;

  • 普通异步任务提交到 TaskQueue 任务队列中 ;

Netty 异步任务调度与异步线程池_第3张图片
Netty 异步任务调度与异步线程池_第4张图片

二、异步线程池

上面使用异步任务调度所使用的的线程仍然是和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());
	}
	...
	}

Netty 异步任务调度与异步线程池_第5张图片

– 求知若饥,虚心若愚。

你可能感兴趣的:(#,Netty,Netty,异步任务调度)