dya*_*nko 43 .net c# webclient async-await asp.net-mvc-4
我有一个非常简单的ASP.NET MVC 4控制器:
public class HomeController : Controller
{
private const string MY_URL = "http://smthing";
private readonly Task<string> task;
public HomeController() { task = DownloadAsync(); }
public ActionResult Index() { return View(); }
private async Task<string> DownloadAsync()
{
using (WebClient myWebClient = new WebClient())
return await myWebClient.DownloadStringTaskAsync(MY_URL)
.ConfigureAwait(false);
}
}
Run Code Online (Sandbox Code Playgroud)
当我启动项目时,我看到了我的视图,它看起来很好,但是当我更新页面时,我收到以下错误:
[InvalidOperationException:在异步操作仍处于挂起状态时完成异步模块或处理程序.
为什么会这样?我做了几个测试:
task = DownloadAsync();从构造函数中删除并将其放入Index方法中它将正常工作而没有错误.DownloadAsync()身体return await Task.Factory.StartNew(() => { Thread.Sleep(3000); return "Give me an error"; });它将正常工作.为什么不可能WebClient.DownloadStringTaskAsync在控制器的构造函数中使用该方法?
Yuv*_*kov 54
在Async Void,ASP.Net和Outstanding Operations中,Stephan Cleary解释了这个错误的根源:
从历史上看,ASP.NET通过基于事件的异步模式(EAP)支持自.NET 2.0以来的干净异步操作,其中异步组件通知SynchronizationContext它们的启动和完成.
发生的事情是你DownloadAsync在你的类构造函数中触发,在你await的异步http调用里面.这将注册与ASP.NET的异步操作SynchronizationContext.当你HomeController返回时,它会看到它有一个尚未完成的挂起异步操作,这就是它引发异常的原因.
如果我们删除task = DownloadAsync(); 从构造函数并将其放入Index方法,它将正常工作,没有错误.
正如我上面解释的那样,那是因为从控制器返回时不再有挂起的异步操作.
如果我们使用另一个DownloadAsync()体返回等待
Task.Factory.StartNew(() => { Thread.Sleep(3000); return "Give me an error"; });它将正常工作.
那是因为Task.Factory.StartNew在ASP.NET中做了一些危险的事情.它不会使用ASP.NET注册任务执行.这可能导致执行池循环的边缘情况,完全忽略后台任务,导致异常中止.这就是为什么你必须使用一个注册任务的机制,例如HostingEnvironment.QueueBackgroundWorkItem.
这就是为什么不可能做你正在做的事情,就像你做的那样.如果你真的希望在后台线程中以"即发即忘"的方式执行它,请使用HostingEnvironment(如果你使用的是.NET 4.5.2)或BackgroundTaskManager.请注意,通过执行此操作,您将使用线程池线程执行异步IO操作,这是多余的,并且正是async-await尝试克服的异步IO .
我今天在构建 API 控制器时遇到了这个错误。事实证明,对于我来说,解决方案很简单。
我有:
public async void Post()
Run Code Online (Sandbox Code Playgroud)
我需要将其更改为:
public async Task Post()
Run Code Online (Sandbox Code Playgroud)
请注意,编译器没有警告async void.
ASP.NET 认为启动与其绑定的“异步操作”SynchronizationContext并ActionResult在所有启动的操作完成之前返回一个是非法的。所有async方法都将自己注册为“异步操作”,因此您必须确保绑定到 ASP.NET 的所有此类调用SynchronizationContext在返回ActionResult.
在您的代码中,您在没有确保DownloadAsync()已运行完成的情况下返回。但是,您将结果保存到task成员中,因此确保这是完整的非常容易。只需await task在返回之前放入所有操作方法(在异步化它们之后):
public async Task<ActionResult> IndexAsync()
{
try
{
return View();
}
finally
{
await task;
}
}
Run Code Online (Sandbox Code Playgroud)
编辑:
在某些情况下,您可能需要调用一个在返回 ASP.NET 之前不应完成的async方法。例如,您可能希望延迟初始化一个后台服务任务,该任务应在当前请求完成后继续运行。OP 的代码不是这种情况,因为 OP 希望任务在返回之前完成。但是,如果您确实需要开始而不是等待任务,则有一种方法可以做到这一点。您只需要使用一种技术来“逃避”当前的.SynchronizationContext.Current
(不推荐) 的一个特性Task.Run()是转义当前的同步上下文。但是,人们建议不要在 ASP.NET 中使用它,因为 ASP.NET 的线程池是特殊的。此外,即使在 ASP.NET 之外,这种方法也会导致额外的上下文切换。
(推荐)在不强制额外的上下文切换或立即打扰 ASP.NET 的线程池的情况下转义当前同步上下文的安全方法是设置SynchronizationContext.Current为null,调用您的async方法,然后恢复原始值。