Netty的BlockingOperationException: DefaultChannelPromise

使用netty传输tcp数据时,小数据量时一切正常,数据量变大时一直抛出异常:

io.netty.util.concurrent.BlockingOperationException: DefaultChannelPromise@3a7c07bb(uncancellable)

定位到问题代码,在ChannelHandler中:

@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) {
    ctx.channel().writeAndFlush(serverResponseMsg).syncUninterruptibly();
}

解决,代码修改为:

@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) {
    ctx.channel().writeAndFlush(serverResponseMsg);
}

writeAndFlush()方法分为两步, 先 write 再 flush,write方法实际上并不是真的将消息写出去, 而是将消息和此次操作的promise放入到了一个队列中,flush才是将数据写出去的地方,即write方法只是把发送数据放入一个缓存,而不会真实的发送,而flush则是将放入缓存的数据发送出去。

syncUninterruptibly()方法:让当前线程同步等待消息发送完成,且无中断异常。

(Netty权威指南和Netty实战中都有谈到,不要在ChannelHandler方法中调用sync()或await()方法,会有可能引起死锁。)

参考1:分析 Netty 死锁异常 BlockingOperationException

current thread调用writeAndFlush方法发送数据并返回一个ChannelFuture,并调用ChannelFuture的await方法进入休眠等待,当发送isDone()时,executor线程池fetch一个线程去唤醒,但如果fetch到current thread就会出现死锁,因为当前线程已经休眠。

我们回忆下wait和notify,调用某个对象的wait()方法能让当前线程阻塞,调用该对象的notify()方法能够唤醒这个个正在等待的线程。wait()、notify()实现的是线程间的协作。

可checkDeadLock是在wait之前进行检测的,那么Netty在current thread进入休眠之前,就应该已经fetch出唤醒thread。可猜测BlockingOperationException异常,可能是executor在fetch线程时将current thread作为notify thread而进行的自我容错检测。

获取当前channel所绑定的eventLoop。如果当前调用线程就是分配给该Channel的EventLoop,代码被执行。否则,EventLoop将task放入一个内部的队列延后执行。

所以,我们大致分析出,在执行write方法时,Netty会判断current thread是否就是分给该Channe的EventLoop,如果是则行线程执行IO操作,否则提交executor等待分配。当执行await方法时,会从executor里fetch出执行线程,这里就需要checkDeadLock,判断执行线程和current threads是否时同一个线程,如果是就检测为死锁抛出异常BlockingOperationException。

那如何解决?官方建议优先使用addListener(GenericFutureListener),而非await()。

// BAD - NEVER DO THIS
@Override
public void channelRead(ChannelHandlerContext ctx, GoodByeMessage msg) {
    ChannelFuture future = ctx.channel().close();
    future.awaitUninterruptibly();
    // Perform post-closure operation
    // ...
}
// GOOD
@Override
public void channelRead(ChannelHandlerContext ctx,  GoodByeMessage msg) {
    ChannelFuture future = ctx.channel().close();
    future.addListener(new ChannelFutureListener() {
        public void operationComplete(ChannelFuture future) {
            // Perform post-closure operation
            // ...
        }
    });
}

参考2:Netty 那些事儿 ——— 心跳机制

当e!=null && e.inEventLoop()为true,则说明执行当前方法的线程就是EventLoop所关联的线程(即,I/O线程)。

checkDeadLock()方法之后就会调用Object的wait()方法。wait()操作会将当前线程挂起,并释放对象锁,直到另一个线程调动该对象的notifyf()或notifyAll()方法,会唤醒一个被挂起的线程。所以如果挂起的线程和调用notify的线程是同一个线程的话,就会发生死锁。(因为线程都已经被挂起了,再给它cpu执行权时也无法执行notify/notifyAll操作,只能另一个线程调用同一个对象的notify去唤醒)
再者在Netty4中,一个Channel对应的所有操作都会在它被创建时分配给它的EventLoop(WorkerEventLoop)中完成,而一个EventLoop的整个生命周期只会和一个线程绑定,不会修改它。
而ChannelFuture则表示Channel异步操作的一个结果。你可以通过ChannelFuture来获取Channel异步操作的结果。获取结果的方式有两种:a) 调用await(*)、sync(*)、get(*) 等方法阻塞当前线程直到获取到异步操作的结果;b) 通过注册回调函数,当操作完成的时候该回调函数会得到调用。
Q:所以,BlockingOperationException到底是在什么情况下会被抛出了?
A:首先,我们已经知道了当执行wait()线程和将会执行notify()/notifyAll()的线程会是同一个线程时,就会造成死锁。
然后,我们知道当ChannelFuture调用await(*)、sync(*)、get(*) 等方法时就会触发当前线程的wait()操作,并将当前线程挂起,等待Channel读写的完成(Channel的读写是EventLoop所关联线程执行的)。
所以,如果执行Channel读写的线程( 即,EventLoop所关联线程,它会调用notify()/notifyAll() ) 和执行ChannelFuture的await(*)、sync(*)、get(*) 等方法的线程( 即,调用wait()的线程 )是同一个线程时,就会发生死锁了!!!
结合示例,Channel提交的任务task最终都将会在EventLoop所关联的线程上得以执行,那么如果你在任务中又调用了await()这样的操作,就会发生上面说所的,挂起的线程和会notify()/notifyAll()的线程是同一个线程,这就会造成死锁。所以Netty在每次执行Object的await()操作之前都会进行判断,判断当前的环境下调用object.await()是否会发送死锁,如果检测任务可能发生死锁,则抛出BlockingOperationException异常,而不会真正的去执行object.await()操作而导致真的死锁发生。
因此,总的来说,我们不应该在Channel所注册到的EventLoop相关联的线程上调用该Channel的ChannelFuture的sync* 或 await* 方法。好像很拗口。。简单图示如下:
Channel_A注册到了EventLoop_A上:Channel_A  —— 注册 ——>  EventLoop_A
ChannelFuture_A表示Channel_A的异步操作:ChannelFuture_A  —— 关联 ——> Channel_A
那么不能在EventLoop_A 上执行 ChannelFuture_A的await(*)、sync(*)、get(*) 等方法。
同时建议,不在ChannelFuture中调用await(*)、sync(*)、get(*) 等方法来获取操作的结果,而是使用注册Listener的方法,通过回调函数来获取操作结果。

 

个人心得:

进入DefaultChannelPromise的syncUninterruptibly()方法,

    public Promise<V> syncUninterruptibly() {
        this.awaitUninterruptibly();
        this.rethrowIfFailed();
        return this;
    }

进入this.awaitUninterruptibly(),

    public Promise<V> awaitUninterruptibly() {
        if (this.isDone()) {
            //如果检测到任务已完成(即消息已发送出去)就直接返回,这应该也是数据量小的时候没有抛出异常的原因
            return this;
        } else {
            //检测是否可能发生死锁
            this.checkDeadLock();
            boolean interrupted = false;
            synchronized(this) {
                while(!this.isDone()) {
                    this.incWaiters();

                    try {
                        //检测通过,且任务没有完成,让当前线程进入等待
                        this.wait();
                    } catch (InterruptedException var9) {
                        interrupted = true;
                    } finally {
                        this.decWaiters();
                    }
                }
            }

            if (interrupted) {
                Thread.currentThread().interrupt();
            }

            return this;
        }
    }

进入this.checkDeadLock(),

    protected void checkDeadLock() {
        EventExecutor e = this.executor();
        if (e != null && e.inEventLoop()) {
            //Channel任务的执行线程就是Channel对应的EventLoop的线程(即当前线程),抛出异常
            throw new BlockingOperationException(this.toString());
        }
    }

进入e.inEventLoop(),进入默认实现AbstractEventExecutor.inEventLoop(),

    public boolean inEventLoop() {
        //任务的执行线程和当前线程进行比较
        return this.inEventLoop(Thread.currentThread());
    }

进入this.inEventLoop(Thread.currentThread()),进入默认实现SingleThreadEventExecutor.inEventLoop(),

    public boolean inEventLoop(Thread thread) {
        //就是比较任务的执行线程和当前线程是否是同一个线程
        return thread == this.thread;
    }

见参考2

个人总结:channelA --> eventLoopA(threadA) --> handlerA(threadA) --> handlerB(threadA),在handler中读写channel中数据时,执行sync()、await()等操作netty会抛出死锁检测异常BlockingOperationException。因为await()会让当前线程进入等待,等待channelA提交的task被对应的任务执行者(threadA)执行完然后notify(),然而在handler中当前线程就是threadA,即当前线程等待自己唤醒自己,显然,当前线程会进入死锁状态。所以,不要在handler中调用sync()、await()等方法。

本质原因还是因为一个channel只会注册到一个EventLoop上,一个EventLoop只会关联一个Thread,所以一个channel只会在一个Thread中处理。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值