Async RabbitMQ消费消息在MVC控制器中阻塞问题求助
问题描述
我尝试通过异步方式从RabbitMQ获取消息并等待返回。控制台应用中调用WaitForMessage方法正常运行,但在MVC控制器中调用时,程序卡在tcs.Task处。通过RabbitMQ UI可确认队列存在待消费消息:
- 两个应用RabbitMQ初始化方式一致,MVC应用已建立连接(RabbitMQ UI可见)
- 调用
BasicConsume后队列无就绪消息,消息状态变为Unacknowledged,关闭应用后消息恢复就绪状态 - 使用RabbitMQ Client版本v6.4.0
附代码:
public async Task<string> WaitForMessage(string queueName) { channel.QueueDeclare(queue: queueName, durable: true, exclusive: false, autoDelete: false, arguments: null); var consumer = new AsyncEventingBasicConsumer(channel); var tag = channel.BasicConsume(queueName, false, consumer); var tcs = new TaskCompletionSource<string>(); consumer.Received += async (sender, eventArgs) => { string message = Encoding.UTF8.GetString(eventArgs.Body.ToArray()); await Task.Yield(); channel.BasicAck(eventArgs.DeliveryTag, false); tcs.SetResult(message); }; string result = await tcs.Task; //.ConfigureAwait(false); return result; }
解决方案
核心原因
MVC控制器中await tcs.Task会捕获当前同步上下文(ASP.NET请求上下文),而AsyncEventingBasicConsumer的Received事件回调默认在RabbitMQ客户端的线程池线程执行,回调里的await Task.Yield()会尝试切回原请求上下文,但此时原上下文因等待tcs.Task处于阻塞状态,最终导致死锁。
修复步骤
- 移除不必要的上下文切换:删掉
await Task.Yield(),回调内无需切回原请求上下文,直接执行后续操作即可。 - 脱离同步上下文等待:给
tcs.Task的await加上.ConfigureAwait(false),避免捕获请求上下文,从根源避免死锁。 - 添加资源清理:任务完成后主动取消消费,防止消费者资源泄漏。
- 增加异常处理:避免因消息处理失败导致
TaskCompletionSource一直处于未完成状态,同时通过BasicNack处理异常消息,防止消息丢失。
修改后的代码:
public async Task<string> WaitForMessage(string queueName) { channel.QueueDeclare(queue: queueName, durable: true, exclusive: false, autoDelete: false, arguments: null); var consumer = new AsyncEventingBasicConsumer(channel); var consumerTag = channel.BasicConsume(queueName, false, consumer); var tcs = new TaskCompletionSource<string>(); consumer.Received += async (sender, eventArgs) => { try { string message = Encoding.UTF8.GetString(eventArgs.Body.ToArray()); channel.BasicAck(eventArgs.DeliveryTag, false); tcs.SetResult(message); } catch (Exception ex) { tcs.SetException(ex); // 异常时拒绝消息并重新入队,避免消息丢失 channel.BasicNack(eventArgs.DeliveryTag, false, true); } }; try { string result = await tcs.Task.ConfigureAwait(false); return result; } finally { // 任务完成后取消消费,释放资源 channel.BasicCancel(consumerTag); } }
额外说明
- 对于传统ASP.NET(非Core),请求上下文是单线程的,捕获上下文后等待极易引发死锁;即便ASP.NET Core没有同步上下文,保持
.ConfigureAwait(false)的写法也能提升异步操作的性能。 - 永远不要忽略异步回调中的异常,否则
TaskCompletionSource会一直处于未完成状态,导致调用方无限等待。
内容的提问来源于stack exchange,提问作者Carl Decks
相关产品推荐
相关产品推荐

