DefaultEventExecutorGroup前瞻
在使用Netty的项目中,经常看到在ChannelPipeline中增加ChannelHandler时有如下的使用方法
scss
pipeline.addLast(
defaultEventExecutorGroup,
new NettyEncoder(),
new NettyDecoder(),
new IdleStateHandler(0, 0, nettyClientConfig.getClientChannelMaxIdleTimeSeconds()),
new NettyConnectManageHandler(),
new NettyClientHandler());
}
在ChannelPipeline调用addLast方法的第一次参数中加上defaultEventExecutorGroup对象,而defaultEventExecutorGroup对象的构造如下
java
this.defaultEventExecutorGroup = new DefaultEventExecutorGroup(
4,
new ThreadFactory() {
private AtomicInteger threadIndex = new AtomicInteger(0);
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "NettyClientWorkerThread_" + this.threadIndex.incrementAndGet());
}
});
可以看到defaultEventExecutorGroup就好像一个线程组一样,构建了4个线程的线程组。 最开始在阅读Netty的代码时,没有很深入的分析这块的使用逻辑,觉得就是当channel有事件发生,channelPipeline驱动ChannelHandler链执行时,从defaultEventExecutorGroup中选择一个线程去执行,后来又深入的熟悉了一下Netty的代码,发现对这块的理解有些偏差,于是想借着这篇文章分析一下defaultEventExecutorGroup和它所关联ChannelHandler的运行逻辑。
DefaultEventExecutorGroup的构造
DefaultEventExecutorGroup是如何被构造出来的,可以先看一下它的具体类关系图

DefaultEventExecutorGroup的其中一个构造函数
csharp
public DefaultEventExecutorGroup(int nThreads, ThreadFactory threadFactory) {
this(nThreads, threadFactory, SingleThreadEventExecutor.DEFAULT_MAX_PENDING_EXECUTOR_TASKS,
RejectedExecutionHandlers.reject());
}
就是传入线程的个数以及线程工厂,然后调用父类的构造器,跟着代码一层一层往上传递,最终会到 MultithreadEventExecutorGroup的构造器,也就是nThreads的个数是几,for循环就会循环几次,创建EventExecutor事件执行器。
ini
children = new EventExecutor[nThreads];
for (int i = 0; i < nThreads; i ++) {
boolean success = false;
try {
children[i] = newChild(executor, args);
success = true;
newChild方法会回调DefaultEventExecutorGroup中的newChild方法用于创建一个EventExecutor,EventExecutor就相当于一个线程不断的从任务队列中取task执行。
java
@Override
protected EventExecutor newChild(Executor executor, Object... args) throws Exception {
return new DefaultEventExecutor(this, executor, (Integer) args[0], (RejectedExecutionHandler) args[1]);
}
EventExecutor中执行任务的逻辑
scss
for (;;) {
Runnable task = takeTask();
if (task != null) {
task.run();
updateLastExecutionTime();
}
if (confirmShutdown()) {
break;
}
}
DefaultEventExecutorGroup的使用
将DefaultEventExecutorGroup与Channelhandler关联起来的操作主要是在ChannelPipeline中,方法如下
scss
public final ChannelPipeline addLast(EventExecutorGroup group, String name, ChannelHandler handler) {
final AbstractChannelHandlerContext newCtx;
synchronized (this) {
checkMultiplicity(handler);
newCtx = newContext(group, filterName(name, handler), handler);
}
EventExecutorGroup作为addLast的参数被传入到方法中,在构建ChannelHandlerContext的方法中继续被当做参数传入
csharp
private AbstractChannelHandlerContext newContext(EventExecutorGroup group, String name, ChannelHandler handler) {
return new DefaultChannelHandlerContext(this, childExecutor(group), name, handler);
}
现在可以看到最终会在childExecutor中被处理,childExecutor方法主要是为ChannelHandler的执行选择执行线程,group要是null的情况下,默认走EventLoop线程,不为null 的情况下从EventExecutorGroup选择一下线程执行器执行
csharp
/**
* 为给定的EventExecutorGroup获取或创建一个子执行器
*
* @param group 事件执行器组,可能为null
* @return 关联的子执行器,如果group为null则返回null
*/
private EventExecutor childExecutor(EventExecutorGroup group) {
// 1. 处理null组的情况
if (group == null) {
return null; // 如果传入的组为null,直接返回null
}
// 2. 检查是否禁用执行器固定功能,默认值是null,大多数情况下默认null为最优选择,本篇文章就是基于null值的分析
Boolean pinEventExecutor = channel.config().getOption(ChannelOption.SINGLE_EVENTEXECUTOR_PER_GROUP);
if (pinEventExecutor != null && !pinEventExecutor) {
return group.next(); // 如果明确配置不固定执行器,则每次返回组的下一个执行器
}
// 3. 获取或初始化执行器映射表
Map<EventExecutorGroup, EventExecutor> childExecutors = this.childExecutors;
if (childExecutors == null) {
// 使用IdentityHashMap确保用对象标识而非equals方法进行比较
childExecutors = this.childExecutors = new IdentityHashMap<EventExecutorGroup, EventExecutor>(4);
}
// 4. 获取或创建组对应的执行器
EventExecutor childExecutor = childExecutors.get(group);
if (childExecutor == null) {
childExecutor = group.next(); // 从组中获取下一个可用执行器
childExecutors.put(group, childExecutor); // 缓存执行器以备后续使用
}
// 5. 返回最终的执行器
return childExecutor;
}
这边可以看到线程执行器EventExecutor已经跟ChannelHandlerContext绑定了,这个ChannelHandler就是使用这个执行器执行,前提是EventExecutorGroup不为null
ini
AbstractChannelHandlerContext(DefaultChannelPipeline pipeline, EventExecutor executor,
String name, Class<? extends ChannelHandler> handlerClass) {
this.name = ObjectUtil.checkNotNull(name, "name");
this.pipeline = pipeline;
this.executor = executor;
this.executionMask = mask(handlerClass);
}
DefaultEventExecutorGroup的关键点
熟悉Netty的都知道,一个NioSocketChannel被创建出来后,会包含与它对应的一个ChannelPipeline,而这个childExecutors属性就是ChannelPipeline中的,这个属性是EventExecutorGroup与选择的执行器的映射
javascript
Map<EventExecutorGroup, EventExecutor> childExecutors
这个映射是在这里构建出来的,也就是说如果EventExecutorGroup相同,则会返回同一个线程执行器
ini
Map<EventExecutorGroup, EventExecutor> childExecutors = this.childExecutors;
if (childExecutors == null) {
childExecutors = this.childExecutors = new IdentityHashMap<EventExecutorGroup, EventExecutor>(4);
}
EventExecutor childExecutor = childExecutors.get(group);
if (childExecutor == null) {
childExecutor = group.next();
childExecutors.put(group, childExecutor);
}
在最开端的这部分代码中,传入一个defaultEventExecutorGroup与这些ChannelHandler关联,所以这个ChannelHandler所使用的执行器线程都是同一个。
当再创建一个NioSocketChannel时,又会创建一个ChannelPipeline,然后这些ChannelHandler又会共享同一个线程执行器,从defaultEventExecutorGroup选择的。
scss
pipeline.addLast(
defaultEventExecutorGroup,
new NettyEncoder(),
new NettyDecoder(),
new IdleStateHandler(0, 0, nettyClientConfig.getClientChannelMaxIdleTimeSeconds()),
new NettyConnectManageHandler(),
new NettyClientHandler());
}
DefaultEventExecutorGroup的总结
对于连接Channel,它的两侧都进行了线程绑定,消息接收线程是 NioEventLoop,业务逻辑处理在 EventExecutor线程中,这样就实现了网络I/O线程与业务逻辑处理线程的绑定,对于某个TCP连接由于双方是一对一的关系,所以降低了锁竞争。
当客户端并发连接比较多时,会有N个连接Channel被并行处理,这样既可以充分利用多核 CPU 的计算资源,又最大程度地降低了锁竞争,提升了系统的并发处理性能。
