繁体   English   中英

如何在 Netty 通道处理程序中安全地执行阻塞操作?

[英]How can you safely perform blocking operations in a Netty channel handler?

我正在构建一个基于 Netty 的小型应用程序,它通过套接字连接(即 telnet/ssh)执行 I/O 操作。 我正在使用 Netty 的ServerBootstrap类启动我的套接字服务器,给它:

  1. NioEventLoopGroup类型的事件循环(即不应受到阻塞操作的共享线程池)。

  2. NioServerSocketChannel类型的通道(我相信这需要与上面的 #1 对应)。

  3. 一个非常简单的管道,带有一个扩展ChannelInboundHandlerAdapter的通道处理程序。

每当从客户端套接字连接接收到命令字符串时,都会调用我的处理程序的channelRead(...)方法,并根据命令返回一些响应字符串。

对于不涉及阻塞操作的命令,一切都很好。 但是,我现在需要从数据库中读取或写入一些命令。 这些 JDBC 调用本质上会阻塞......尽管我可以使用CompletableFuture (或其他)在单独的线程中处理它们。

但是,即使我通过在单独的线程中执行阻塞操作来“滚动我自己的异步”,我也不确定如何将这些产生的线程的结果重新连接回主线程中的 Netty 通道处理程序。

我看到ChannelHandlerContext类具有如下方法:

ChannelFuture writeAndFlush(Object msg, ChannelPromise promise);

...作为我目前使用的替代品:

ChannelFuture writeAndFlush(Object msg);

但是我找不到任何文档或指南(甚至有用的 Javadocs)来解释如何在这个用例中使用这个ChannelPromise类型。 它的名字表明它可能是相关的,但也可能不是。 毕竟, writeAndFlush方法仍然将传出消息作为它的第一个参数......那么如果你需要它的结果已经在手头上,那么将你的阻塞操作塞进一个“承诺”的第二个参数有什么好处呢?第一个参数?

这里的正确轨道是什么? 有没有办法在单独的线程中处理阻塞操作,让Netty的NioEventLoopGroup不阻塞? 或者这根本不是 Netty 的工作方式,如果您需要支持阻塞,您应该使用不同的事件循环实现(即为每个客户端套接字连接生成一个单独的线程)?

我正在构建一个基于 Netty 的小型应用程序,它通过套接字连接(即 telnet/ssh)执行 I/O 操作。 我正在使用 Netty 的ServerBootstrap类启动我的套接字服务器,给它:

  1. NioEventLoopGroup类型的事件循环(即不应受到阻塞操作的共享线程池)。

  2. NioServerSocketChannel类型的通道(我相信这需要与上面的 #1 对应)。

  3. 一个非常简单的管道,带有一个扩展ChannelInboundHandlerAdapter的通道处理程序。

每当从客户端套接字连接接收到命令字符串时,都会调用我的处理程序的channelRead(...)方法,并根据命令返回一些响应字符串。

对于不涉及阻塞操作的命令,一切都很好。 但是,我现在需要从数据库中读取或写入一些命令。 这些 JDBC 调用本质上会阻塞......尽管我可以使用CompletableFuture (或其他)在单独的线程中处理它们。

但是,即使我通过在单独的线程中执行阻塞操作来“滚动我自己的异步”,我也不确定如何将这些产生的线程的结果重新连接回主线程中的 Netty 通道处理程序。

我看到ChannelHandlerContext类具有如下方法:

ChannelFuture writeAndFlush(Object msg, ChannelPromise promise);

...作为我目前使用的替代品:

ChannelFuture writeAndFlush(Object msg);

但是我找不到任何文档或指南(甚至有用的 Javadocs)来解释在这个用例中如何使用这个ChannelPromise类型。 它的名字表明它可能是相关的,但也可能不是。 毕竟, writeAndFlush方法仍然将传出消息作为它的第一个参数......那么如果你需要它的结果已经在手头上,那么把你的阻塞操作塞进一个“承诺”的第二个参数有什么好处呢?第一个参数?

这里的正确轨道是什么? 有没有办法在单独的线程中处理阻塞操作,让Netty的NioEventLoopGroup不阻塞? 或者这根本不是 Netty 的工作方式,如果您需要支持阻塞,您应该使用不同的事件循环实现(即为每个客户端套接字连接生成一个单独的线程)?

即使专用的 DefaultEventExecutorGroup 与处理程序相关联,似乎也永远不允许在 netty 线程上运行阻塞操作。

DefaultEventExecutorGroup(16) 的问题在于它由固定数量的 DefaultEventExecutor 组成,这意味着可以将相同的 DefaultEventExecutor 分配给多个通道,因此对一个通道处理程序的阻塞操作将冻结分配给该处理程序的所有其他通道处理程序的处理相同的 DefaultEventExecutor。 您可以开发一个简单的测试来检查:

import io.netty.util.concurrent.DefaultEventExecutorGroup;

public class DefaultEventExecutorMain {
    public static void main(String... args) {
        var group = new DefaultEventExecutorGroup(2);

        var eventExecutor1 = group.next();
        var eventExecutor2 = group.next();
        var eventExecutor3 = group.next();

        eventExecutor1.execute(() -> {
            System.out.println("slow task start");
            try {
                Thread.sleep(60000);
            } catch (InterruptedException t) {
                throw new RuntimeException(t);
            }
            System.out.println("slow task end");
        });

        eventExecutor3.execute(() -> {
            System.out.println("fast task");
        });
    }
}

慢任务将使快任务等待,因为两者都被分配给同一个事件执行器,该事件执行器坚持单个线程。

从理论上讲,可以开发自定义事件执行器,它不会粘在特定线程上,只会确保所有提交的任务都按顺序执行。 像这样的东西:

public class MyEventExecutor extends AbstractEventExecutor {

    private volatile Thread executorThread;

    private final Queue<Runnable> queue = new ArrayDeque<>();
    private final ReentrantLock lock = new ReentrantLock();
    private final Executor executor;
    private final Logger logger = Logger.getLogger(this.getClass().getName());

    private Task task;

    public MyEventExecutor(EventExecutorGroup parent, Executor executor) {
        super(parent);
        this.executor = executor;
    }

    @Override
    public void execute(Runnable runnable) {
        lock.lock();
        try {
            queue.add(runnable);

            if (task == null) {
                task = new Task();
                executor.execute(task);
            }
        } finally {
            lock.unlock();
        }
    }

    private class Task implements Runnable {
        @Override
        public void run() {
            Runnable r;

            lock.lock();
            try {
                r = queue.poll();
            } finally {
                lock.unlock();
            }

            executorThread = Thread.currentThread();
            try {
                r.run();
            } catch (Throwable t) {
                logger.log(Level.SEVERE, "Unhandled failure", t);
            } finally {
                executorThread = null;

                lock.lock();
                try {
                    if (queue.isEmpty()) {
                        task = null;
                    } else {
                        executor.execute(task);
                    }
                } finally {
                    lock.unlock();
                }
            }
        }
    }

和自定义事件执行组:

public class MyEventExecutorGroup extends AbstractEventExecutorGroup {

    private Executor executor;

    public MyEventExecutorGroup(Executor executor) {
        this.executor = executor;
    }

    @Override
    public EventExecutor next() {
        return new MyEventExecutor(this, executor);
    }

假设作为参数传递的执行程序是线程池执行程序,可以并行运行多个任务。

但最好永远不要这样做,只需使用自定义线程池执行器在 netty 线程之外运行阻塞任务。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM