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

相关推荐
玩代码1 小时前
CompletableFuture 详解
java·开发语言·高并发·线程
人生在勤,不索何获-白大侠2 小时前
day21——特殊文件:XML、Properties、以及日志框架
xml·java·开发语言
free-9d4 小时前
NodeJs后端常用三方库汇总
后端·node.js
Dcs4 小时前
用不到 1000 行 Go 实现 BaaS,Pennybase 是怎么做到的?
java
Cyanto6 小时前
Spring注解IoC与JUnit整合实战
java·开发语言·spring·mybatis
qq_433888936 小时前
Junit多线程的坑
java·spring·junit
gadiaola6 小时前
【SSM面试篇】Spring、SpringMVC、SpringBoot、Mybatis高频八股汇总
java·spring boot·spring·面试·mybatis
写不出来就跑路6 小时前
WebClient与HTTPInterface远程调用对比
java·开发语言·后端·spring·springboot
Cyanto6 小时前
深入MyBatis:CRUD操作与高级查询实战
java·数据库·mybatis
麦兜*7 小时前
Spring Boot 集成Reactive Web 性能优化全栈技术方案,包含底层原理、压测方法论、参数调优
java·前端·spring boot·spring·spring cloud·性能优化·maven