关于Netty的DefaultEventExecutorGroup使用

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 的计算资源,又最大程度地降低了锁竞争,提升了系统的并发处理性能。

相关推荐
不太可爱的叶某人32 分钟前
【学习笔记】深入理解Java虚拟机学习笔记——第7章 虚拟机类加载机制
java·笔记·学习
我的炸串拌饼店36 分钟前
ASP.NET MVC 中SignalR实现实时进度通信的深度解析
后端·asp.net·mvc
CUIYD_19891 小时前
Spring MVC 处理静态资源请求 - ResourceHandler
java·spring·mvc
晴空月明1 小时前
Java 集合框架底层数据结构实现深度解析
java
louisgeek1 小时前
Java Creational 创建型模式之 Builder 建造者模式
java
挑战者6668881 小时前
springboot入门之路(一)
java·spring boot·后端
重整旗鼓~2 小时前
7.索引库操作
java·开发语言
云心雨禅3 小时前
Spring Boot热更新技巧:节省90%重启时间
java·数据库·spring boot
wmze3 小时前
InnoDB存储引擎
后端
岁忧3 小时前
(LeetCode 每日一题) 2966. 划分数组并满足最大差限制 (贪心、排序)
java·c++·算法·leetcode·职场和发展·go