.NET Core中Scoped服务ISessionService异步初始化方案咨询
.Result死锁的问题 首先得说,你现在遇到的问题非常典型——在ASP.NET Core里用.Result同步等待异步操作,很容易触发线程死锁,尤其是高负载场景下,大量线程被阻塞会直接导致应用挂起。根源在于ASP.NET Core的请求上下文(即使是.NET Core 3.0+的简化上下文)会限制异步操作完成后回到原线程,阻塞的线程握着上下文不放,异步任务就没法完成,形成死锁。
下面给你两种靠谱的解决方案,帮你彻底摆脱.Result的坑:
方案1:异步初始化+请求中间件(推荐)
这种方式是在请求处理流程的早期就完成SessionService的异步初始化,后续代码可以直接同步访问属性,不用再处理异步逻辑。
步骤1:定义异步初始化接口
先创建一个通用的异步初始化接口,方便后续其他服务复用:
public interface IAsyncInitializable { Task InitializeAsync(); }
步骤2:改造SessionService实现接口
把原来的同步属性改成延迟加载,初始化逻辑移到InitializeAsync方法里:
public class SessionService : ISessionService, IAsyncInitializable { private readonly YourDbContext _dbContext; private readonly IHttpContextAccessor _httpContextAccessor; private ICompany _cachedCompany; public SessionService(YourDbContext dbContext, IHttpContextAccessor httpContextAccessor) { _dbContext = dbContext; _httpContextAccessor = httpContextAccessor; } // 异步初始化方法,专门处理需要await的数据库查询 public async Task InitializeAsync() { var host = _httpContextAccessor.HttpContext.Request.Host.Host; _cachedCompany = await _dbContext.GetCompany(host); } // 同步属性,直接返回已初始化好的实例 public ICompany Company => _cachedCompany ?? throw new InvalidOperationException("SessionService未完成初始化,请确保请求中间件已调用InitializeAsync"); }
步骤3:注册中间件确保每次请求初始化
在Program.cs里添加一个中间件,确保每个请求的Scoped SessionService都完成异步初始化:
app.Use(async (context, next) => { // 从当前请求的服务容器中获取SessionService if (context.RequestServices.GetService<ISessionService>() is IAsyncInitializable initializableService) { await initializableService.InitializeAsync(); } await next(); }); // 注意:这个中间件要放在其他业务中间件之前,比如UseRouting、UseAuthorization之前
这种方式的好处是,所有业务代码可以像原来一样直接访问SessionService.Company,不用修改调用逻辑,同时彻底避免了同步阻塞。
方案2:延迟异步加载(Lazy)
如果不想在请求早期就完成初始化,或者某些请求可能不需要用到Company属性,可以用Lazy<Task<ICompany>>实现延迟异步加载,只有第一次访问的时候才触发数据库查询。
改造后的SessionService代码:
public class SessionService : ISessionService { private readonly YourDbContext _dbContext; private readonly IHttpContextAccessor _httpContextAccessor; private readonly Lazy<Task<ICompany>> _lazyCompany; public SessionService(YourDbContext dbContext, IHttpContextAccessor httpContextAccessor) { _dbContext = dbContext; _httpContextAccessor = httpContextAccessor; // 用Lazy包装异步任务,确保只执行一次数据库查询 _lazyCompany = new Lazy<Task<ICompany>>(async () => { var host = _httpContextAccessor.HttpContext.Request.Host.Host; return await _dbContext.GetCompany(host); }); } // 提供异步方法让调用方获取Company public Task<ICompany> GetCompanyAsync() { return _lazyCompany.Value; } }
然后调用方需要修改代码,用await获取:
// 原来的同步调用:var company = sessionService.Company; // 改成异步调用: var company = await sessionService.GetCompanyAsync();
这种方式的好处是按需加载,避免不必要的数据库查询,但需要所有调用方都适配异步逻辑。
为什么原来的写法会挂起?
再补充一下为什么.Result会出问题:当你调用.Result时,当前线程会阻塞等待异步任务完成。而_dbContext.GetCompany(host)是异步操作,它完成后需要回到原请求上下文继续执行后续逻辑,但原线程正握着上下文阻塞等待,这就形成了死锁——异步任务等上下文,阻塞线程等异步任务,高负载下大量这种死锁线程会耗尽线程池,导致应用无法处理新请求,最终挂起。
内容的提问来源于stack exchange,提问作者Paul

