You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

MediatR Publish()与ToListAsync()结合时出现阻塞问题求助

MediatR Publish() 结合 ToListAsync() 出现阻塞问题排查

当MediatR的Publish()方法与ToListAsync()结合使用时会出现阻塞(任务始终无法完成),但使用同步的ToList()时却能正常工作。以下是最小复现示例(因依赖数据库无法直接运行):

namespace ShowcasePublishGetsStuck
{
    using System;
    using System.Linq;
    using System.Threading;
    using System.Threading.Tasks;
    using Database;
    using Database.Enums;
    using Database.Models;
    using MediatorEvents.Notifications;
    using MediatR;
    using Microsoft.EntityFrameworkCore;

    public class ShowcaseToListAsyncStuck
    {
        private MyDbContext _db;
        private readonly IMediator _mediator;

        public ShowcaseToListAsyncStuck(
            MyDbContext db,
            IMediator mediator)
        {
            _mediator = mediator;

            DemoGettingStuck(CancellationToken.None).Wait(); // 此处使用Wait()是核心问题诱因
        }

        public async Task<bool> DemoGettingStuck(CancellationToken cancellationToken)
        {
            await _mediator.Publish(new KycCompanyCreatedEvent(0), cancellationToken);
            
            _mediator.Publish(new KycCompanyCreatedEvent(0), cancellationToken).Wait(cancellationToken);
            
            for (int i = 0; i < 1; i++)
            {
                await FunctionThatCallsSimplePublish(cancellationToken); // 正常执行
            }

            var purchasesSync = _db.Purchases
                .Where(x => x.Id == 405)
                .ToList();

            foreach (var _ in purchasesSync)
            {
                await FunctionThatCallsSimplePublish(cancellationToken); // 正常执行
            }
            
            var purchasesAsync = await _db.Purchases
                .Where(x => x.Id == 405)
                .ToListAsync(cancellationToken); // 执行此方法后后续代码阻塞

            foreach (var _ in purchasesAsync)
            {
                await FunctionThatCallsSimplePublish(cancellationToken); // 永远无法执行到此处
            }

            return true; // 永远无法执行到此处
        }
        
        public async Task FunctionThatCallsSimplePublish(CancellationToken cancellationToken = default)
        {
            await _mediator.Publish(new KycCompanyCreatedEvent(0), cancellationToken);
            await Task.Delay(0);
        }
    }
}

环境信息

  • .NET 6
  • EntityFrameworkCore NuGet包版本:6.0.10
  • MediatR版本:11.0.0(也尝试过12.1.1,问题复现)

处理程序代码

[UsedImplicitly]
public class KycCompanyCreatedEventHandler : INotificationHandler<KycCompanyCreatedEvent>
{
    private readonly IMyProvider _myProvider;
    private readonly MyDbContext _db;

    public KycCompanyCreatedEventHandler(IMyProvider myProvider, MyDbContext db)
    {
        _myProvider = myProvider;
        _db = db;
    }

    public Task Handle(KycCompanyCreatedEvent notification, CancellationToken cancellationToken)
    {
        return Task.CompletedTask; // 使用ToListAsync()时,此处断点永远不会触发
    }
}

问题根源

这不是MediatR或EF Core的Bug,而是混合同步阻塞与异步操作导致的线程死锁:

  1. 构造函数中调用DemoGettingStuck(...)的.Wait()方法,会阻塞当前线程(通常是主线程或ASP.NET请求上下文线程)。
  2. 在同步上下文环境(比如ASP.NET)中,await会捕获当前上下文,后续异步操作完成后需要回到该上下文继续执行。
  3. 当执行ToListAsync()后,后续的_mediator.Publish()异步操作需要回到被.Wait()阻塞的上下文,此时上下文线程被占用,异步任务无法继续执行,从而形成死锁。
  4. 使用同步的ToList()时,不会切换上下文,因此后续的异步操作能正常执行。

解决方案

  • 禁止在构造函数中阻塞异步方法:构造函数是同步执行的,不要用.Wait()/.Result()等方式阻塞异步逻辑。可以通过异步初始化接口(如ASP.NET的IAsyncInitializer)或者在合适的异步生命周期钩子中执行异步代码。
  • 全程使用异步await:避免同步阻塞异步任务,保持异步代码的一致性,消除死锁的根源。
  • 检查DI生命周期配置:确保MediatR处理程序、DbContext的生命周期配置合理,避免因生命周期冲突加剧上下文阻塞问题。

内容的提问来源于stack exchange,提问作者CodingYourLife

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.10 19:14:59