为何无Task.Run()时ASP.NET会挂起?求更优实现方案
问题描述
在单元测试或控制台应用中调用以下方法时运行正常:
public Application GetApplication(string applicationName) { var application = _oktaApplicationApi.ListApplications(q: applicationName, limit: 1).SingleOrDefaultAsync().Result; return application; }
但在ASP.NET中调用该方法时会出现挂起问题。为使其正常运行,目前采用Task.Run()包装异步调用的写法:
public Application GetApplication(string applicationName) { Application application; var t = Task.Run(async () => { var a = await _oktaApplicationApi.ListApplications(q: applicationName, limit: 1).SingleOrDefaultAsync(); return a; }); application = t.Result; return application; }
请问:
- 导致ASP.NET中出现挂起的原因是什么?
- 是否有更简洁的写法可以避免该挂起问题?
注:尽管可以将GetApplication()改为异步方法,但该方法是从非异步的自定义MembershipProvider中调用的。
问题解答
一、挂起原因
这是典型的异步代码同步阻塞导致的死锁,核心和ASP.NET的SynchronizationContext(同步上下文)有关:
- 在传统ASP.NET(非Core版本)中,每个请求会绑定一个专属的同步上下文,该上下文同一时间仅允许一个线程执行。
- 调用
.Result时,当前线程会阻塞,等待异步任务完成。 - 异步任务执行结束后,需要回到原同步上下文继续后续逻辑,但此时原线程正处于阻塞状态、占用着上下文,导致任务无法收尾,双方互相等待形成死锁。
- 单元测试和控制台应用没有这种专属同步上下文,异步任务完成后会直接在线程池线程继续执行,因此不会触发死锁。
二、更简洁的写法
写法1:用ConfigureAwait(false)规避上下文捕获
在异步调用链中添加ConfigureAwait(false),让异步任务完成后不回到原同步上下文,直接在线程池线程执行,避免死锁:
public Application GetApplication(string applicationName) { var application = _oktaApplicationApi.ListApplications(q: applicationName, limit: 1) .SingleOrDefaultAsync() .ConfigureAwait(false) .GetAwaiter() .GetResult(); return application; }
这种写法比Task.Run更轻量,无需额外开启线程池线程。
写法2:简化Task.Run的代码结构
如果倾向于保留Task.Run的方式,可以简化代码:
public Application GetApplication(string applicationName) { return Task.Run(async () => await _oktaApplicationApi.ListApplications(q: applicationName, limit: 1) .SingleOrDefaultAsync() .ConfigureAwait(false)) .Result; }
内容的提问来源于stack exchange,提问作者Mark Good
相关产品推荐
相关产品推荐

