使用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 syncUninterruptibly() {
this.awaitUninterruptibly();
this.rethrowIfFailed();
return this;
}
进入this.awaitUninterruptibly(),
public Promise 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中处理。