Netty21——Netty中处理耗时任务的方式

本文介绍了在Netty中如何处理耗时任务以避免影响Socket处理速度。主要讨论了两种方法:1) 在Handler中添加线程池,将耗时操作异步化;2) 在Context中配置线程池,让Handler的执行在线程池中进行。第一种方式更为灵活,可以根据需要决定是否异步,但可能会增加响应时间。第二种方式则是Netty的标准实践,但所有Handler操作都会异步,不够灵活。选择哪种方式取决于具体业务需求。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 如果在 Netty 中做耗时的、不可预料的操作,比如数据库、网络请求等,会严重影响 Netty 对 Socket 的处理速度。解决方法就是将耗时任务添加到异步线程池中。但就添加线程池这步操作来讲,有2种方式:
 在Handler 中加入线程池;
 在Context 中添加线程池;

一、在Handler中将任务加入线程池

 将EchoServerHandler的channelRead()方法进行修改,该为异步的方式:

@Sharable
public class EchoServerHandler extends ChannelInboundHandlerAdapter{
	static final EventExecutorGroup group = new DefaultEventExecutorGroup(16);

	@Override
	public void channelRead(ChannelHandlerContext ctx,Object msg) throws UnsupportedEncodingException,InterruptedException{
		final Object msgCop = msg;
		//缓存上下文对象
		final ChannelHandlerContext ctxCop = ctx;
		//将耗时任务加入到异步线程池
		group.submit(new Callable<Object>(){
			@Override
			public Object call() throws Exception{
				ByteBuf buf = (ByteBuf)msgCop;
				byte[] req = new byte[buf.readableBytes()];
				buf.readBytes(req);
				String body = new String(req,"UTF-8");
				Thread.sleep(10000);// 模拟慢操作
				System.err.println(body + "" + Thread.currrentThread().getName());
				String reqString = "Hello i am server...";
				ByteBuf resp = UnpooledcopiedBuffer(reqString.getBytes());
				cxtCop.writeAndFlush(resp);
				return null;
			}
		});
		System.out.println("go on ...");
	}

	@Override
	public void channelReadComplete(ChannelHandlerContext ctx){
		ctx.flush();
	}
	
	@Override
	public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause){
		//Close the connection when an exeception is raised
		cause.printStackTrace();
		ctx.close();
	}
}

 在channelRead()中,我们将耗时的任务提交到了一个自定义的业务线程池中,这样就不会阻塞Netty自身的IO线程了。
在这里插入图片描述
 当IO线程轮询到一个socket事件,然后IO线程开始处理,当处理到耗时handler的时候,handler将耗时任务交给业务线程池处理。当耗时任务执行完毕再执行pipeline 的write方法的时候(代码中使用的是context的writeAndFlush方法——最终会调用下面的write(),上图画的是pipeline方法,是一个意思)会将这个任务交给IO线程。

private void write(Object msg, boolean flush, ChannelPromise promise) {
    AbstractChannelHandlerContext next = findContextOutbound();
    final Object m = pipeline.touch(msg, next);
    EventExecutor executor = next.executor();
    if (executor.inEventLoop()) {//没有将任务放入自定义的线程池时该处的值就会是true
        if (flush) {
            next.invokeWriteAndFlush(m, promise);
        } else {
            next.invokeWrite(m, promise);
        }
    } else {//将任务放入自定义的线程池时会走下面的逻辑
        AbstractWriteTask task;
        if (flush) {
            task = WriteAndFlushTask.newInstance(next, m, promise);
        }  else {
            task = WriteTask.newInstance(next, m, promise);
        }
        safeExecute(executor, task, promise, m);
    }
}

 当判定下个outbound的executor线程不是当前线程的时候,会将当前的工作封装成task,然后放入mpsc队列中,等待IO任务执行完毕后执行队列中的任务。若我们在上面的Handler中将任务加入到了自定义的线程池中,就会走safeExecute(executor, task, promise, m),将任务放入mpsc队列中。

二、在Context中将任务加入线程池

 在向pipeline中添加handler的时候,添加一个线程池,将指定handler的业务处理交给这个线程池完成:

static final EventExecutorGroup group = new DefaultEventExecutorGroup(16);
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup,worderGroup)
	.channel(NioServerSocketChannel.class)
	.option(ChannelOption.SO_BACKLOG,100)
	.handler(new LoggingHandler(LogLevel.INFO))
	.childHandler(new ChannelInitializer<SocketChannel>(){
		@Override
		public void initChannel(SocketChannel ch) throws Exception{
			ChannelPipeline p = ch.pipeline();
			if(sslCtx != null){
				p.addLast(sslCtx.newHandler(ch.alloc()));
			}
			// 将EchoServerHandler的执行交给group线程池处理
			p.addLast(group,new EchoServerHandler());
		}
	});

 这样,handler中(上例中的EchoServerHandler)的代码就可以使用普通的方式来处理耗时业务,用户就不必在handler中将耗时任务加入到线程池中了。在调用addLast()方法添加线程池后,handler将优先使用这个线程池,如果这里没有添加这个线程池,将会使用IO线程。
 当代码执行到AbstractChannelHandlerContext的invokeChannelRead()方法的时候,executor.inEventLoop()是不会通过的,因为当前线程是IO线程,Context(或者说Handler)的executor是业务线程(另外一个线程),所以会异步执行。

static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) {
    final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next);
    EventExecutor executor = next.executor();
    if (executor.inEventLoop()) {
        next.invokeChannelRead(m);
    } else {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                next.invokeChannelRead(m);
            }
        });
    }
}

 如果我们将p.addLast(group,new EchoServerHandler())改为p.addLast(new EchoServerHandler()),会发现上述代码不会异步执行,后面的整个流程就和第一种方式一样了。

三、两种方式的比较

 1、第一种方式在Handler中添加异步,可能更加的灵活。比如如果需要访问数据库,那我就异步,如果不需要,就不异步。异步会拖长接口响应时间,因为需要将任务放进mpscTask中。如果IO时间很短,task很多,可能一个循环下来,都没有时间执行整个task,导致响应时间达不到要求。
 2、第二种方式是Netty的标准方式,但这会将整个Handler都交给业务线程池,不论耗不耗时,不够灵活。
 使用时,需要自己根据业务斟酌。

<think>嗯,用户这次的问题聚焦在Netty的高并发线程模型和NIO机制上,而且明确要求"通俗解释"。看来ta可能是个开发者,正在学习网络编程或者面临实际的高并发场景优化需求。从ta引用的资料看,已经对BIO/NIO基础有了解,但需要更落地的实现原理剖析。 用户引用的资料[2][4][5]特别关键,其中提到Reactor模式和Netty的线程模型关系。需要把这些分散的知识点串联起来:首先解释传统IO的瓶颈(引用[3]提到百万级并发时BIO的无能为力),然后用生活化比喻过渡到NIO的核心改进——比如把阻塞的"一对一服务"变成"餐厅叫号系统"。 关于线程模型部分,主从Reactor多线程模式是重点(引用[2]的2.5节)。需要强调两个关键设计:1) bossGroup只负责"接客"不处理业务,2) workerGroup用线程池避免线程爆炸。这和引用[5]说的"多路复用器轮询"机制要对应起来。 用户可能混淆的概念点:NIO的Selector在Netty中如何封装?需要说明ChannelHandlerContext和Pipeline的关系,就像流水线工人分工装配零件。另外务必解释"无锁串行化设计"为什么能避免并发问题——这点在引用[4]的Netty简介里提到但没展开。 最后要关联实际场景,比如引用[1]说的实时系统或大数据处理。用"百万连接聊天服务器"为例,说明EventLoop如何用少量线程支撑:每个EventLoop相当于一个全能服务员,通过状态位判断哪些桌位(channel)需要服务,而不是傻等。 特别注意用户要求"通俗",所以要把Buffer/Selector/Channel的三角关系比喻成"快递仓库-调度员-传送带"。技术术语如epoll可以提,但重点说效果而非实现。</think>以下是Netty在高并发场景下的线程模型及NIO机制工作原理的通俗解析,结合关键概念逐步说明: --- ### 一、传统IO的瓶颈(背景知识) - **同步阻塞(BIO)**:每个连接需独占一个线程,当十万级连接时,线程资源耗尽且频繁切换导致性能崩溃[^3]。 - **问题本质**:线程资源浪费在等待I/O上,无法应对海量连接。 --- ### 二、NIO的核心机制:多路复用 1. **同步非阻塞** - 线程不再阻塞等待数据,而是**轮询**检查通道状态(类似快递员批量扫描包裹)。 - 关键组件: - **Channel(通道)**:数据传输管道(如文件、网络套接字) - **Buffer(缓冲区)**:数据临时存储区(类似集装箱) - **Selector(选择器)**:核心调度员,单线程监控多个Channel事件[^5]。 2. **工作流程** ```mermaid graph LR A[客户端连接] --> B(注册到Selector) B --> C{Selector轮询} C -->|有读事件| D[读取数据到Buffer] C -->|有写事件| E[从Buffer写入数据] ``` - 一个Selector线程可处理成千上万的连接,避免线程爆炸[^3][^5]。 --- ### 三、Netty的线程模型:主从Reactor多线程 > **设计目标**:最大化利用CPU资源,避免锁竞争 #### 1. **核心组件** - **BossGroup**(主Reactor) - 负责接收新连接(相当于前台接待) - 默认1个EventLoop(线程)即可处理所有接入请求 - **WorkerGroup**(从Reactor) - 处理I/O读写和业务逻辑(相当于业务处理小组) - 多个EventLoop(线程)并行工作 #### 2. **工作流程** ```mermaid graph TB Client -->|新连接| BossGroup BossGroup -->|注册Channel| WorkerGroup WorkerGroup -->|分配EventLoop| Handler[处理读写/业务] ``` - **关键机制**: - **无锁串行化设计**:每个Channel绑定固定EventLoop,避免多线程竞争[^2][^4]。 - **任务队列**:耗时操作(如数据库查询)放入队列,由其他线程异步处理,防止阻塞EventLoop[^2]。 #### 3. **百万级并发支撑原理** - **少量线程处理海量连接** 通过Selector监控所有Channel状态,仅在有实际I/O时唤醒线程(事件驱动)。 > 例:10个EventLoop可处理10万连接,而BIO需要10万个线程。 - **零拷贝优化** 直接操作内存缓冲区,减少数据在JVM堆与系统内存间的复制[^4]。 --- ### 四、Netty vs 原生NIO | 特性 | 原生NIO | Netty | |--------------|----------------------------|--------------------------------| | 线程模型 | 需手动实现Reactor | 内置主从Reactor多线程 | | API复杂度 | 复杂(需处理拆包/粘包) | 简化API,提供编解码框架 | | 资源管理 | 易内存泄漏 | 自动内存回收机制 | | 性能 | 需优化 | 零拷贝、无锁设计优化[^4] | --- ### 五、典型应用场景 1. **实时通信系统** - 微信/QQ等IM工具:百万用户同时在线消息推送[^1]。 2. **游戏服务器** - 王者荣耀:大量玩家低延迟交互[^5]。 3. **金融交易系统** - 股票交易:每秒万级订单处理[^1]。 > ✅ **总结**:Netty通过**主从Reactor线程模型** + **NIO多路复用** + **无锁串行化**,用少量线程支撑海量连接,成为高并发场景首选框架[^2][^4]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值