ASP.NET控制器:异步模块或处理程序已完成,而异步操作仍处于挂起状态
本文关键字:异步操作 挂起状态 已完成 程序 控制器 NET 异步 模块 处理 ASP | 更新日期: 2024-09-25 20:13:41
我有一个非常简单的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);
}
}
当我开始项目时,我看到了我的视图,它看起来很好,但当我更新页面时,我得到了以下错误:
[InvalidOperationException:异步模块或处理程序已完成,而异步操作仍处于挂起状态。]
为什么会发生这种情况?我做了几个测试:
- 如果我们从构造函数中删除
task = DownloadAsync();
并将其放入Index
方法中,它将正常工作,不会出现错误 - 如果我们使用另一个
DownloadAsync()
主体return await Task.Factory.StartNew(() => { Thread.Sleep(3000); return "Give me an error"; });
,它将正常工作
为什么不可能在控制器的构造函数中使用WebClient.DownloadStringTaskAsync
方法?
在Async Void、ASP.Net和Count of Outstanding Operations中,Stephan Cleary解释了此错误的根源:
从历史上看,ASP.NET一直支持干净的异步操作由于.NET 2.0通过基于事件的异步模式(EAP),哪些异步组件通知SynchronizationContext他们的开始和完成
所发生的情况是,您在类构造函数中激发DownloadAsync
,在异步http调用中,在类构造函数内激发await
。这将向ASP.NET SynchronizationContext
注册异步操作。当HomeController
返回时,它会看到它有一个尚未完成的挂起异步操作,这就是它引发异常的原因。
如果我们移除task=DownloadAsync();从构造函数中,并将其放入到Index方法中,它将正常工作而不会出现错误。
正如我在上面解释的,这是因为从控制器返回时不再有挂起的异步操作。
如果我们使用另一个DownloadAsync()主体返回wait
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试图克服的问题。
ASP.NET认为在所有启动的操作完成之前启动绑定到其SynchronizationContext
的"异步操作"并返回ActionResult
是非法的。所有async
方法都将自己注册为"异步操作",因此必须确保绑定到ASP.NET SynchronizationContext
的所有此类调用在返回ActionResult
之前完成。
在您的代码中,您返回时没有确保DownloadAsync()
已经运行完成。但是,您可以将结果保存到task
成员中,因此确保这是非常容易的。在返回之前,只需在所有操作方法中放入await task
(异步化后):
public async Task<ActionResult> IndexAsync()
{
try
{
return View();
}
finally
{
await task;
}
}
编辑:
在某些情况下,您可能需要调用async
方法,在返回到ASP.NET之前不应完成该方法。例如,您可能希望延迟初始化后台服务任务,该任务应在当前请求完成后继续运行。OP的代码不是这样的,因为OP希望任务在返回之前完成。然而,如果你确实需要开始而不是等待任务,有一种方法可以做到这一点。您只需要使用一种技术来"逃离"当前的SynchronizationContext.Current
。
(未重新开始)
Task.Run()
的一个功能是逃离当前同步上下文。但是,人们建议不要在ASP.NET中使用它,因为ASP.NET的线程池是特殊的。此外,即使在ASP.NET之外,这种方法也会导致额外的上下文切换。(推荐)在不强制进行额外上下文切换或立即干扰ASP.NET线程池的情况下,逃离当前同步上下文的一种安全方法是将
SynchronizationContext.Current
设置为null
,调用async
方法,然后恢复原始值。
我今天在构建API控制器时遇到了这个错误。事实证明,在我的情况下,解决方案很简单。
我有:
public async void Post()
我需要把它改成:
public async Task Post()
请注意,编译器没有对async void
发出警告。
我遇到了相关问题。客户端正在使用一个返回Task的接口,该接口是用async实现的。
在Visual Studio 2015中,异步的客户端方法在调用该方法时不使用await关键字,不会收到警告或错误,代码编译干净。比赛状态被提升到生产状态。
方法返回async Task
,而ConfigureAwait(false)
可以是其中一个解决方案。它的行为就像async void,不会继续同步上下文(只要你真的不关心方法的最终结果)
myWebClient方法。DownloadStringTaskAsync在单独的线程上运行,并且是非阻塞的。一个可能的解决方案是使用myWebClient的DownloadDataCompleted事件处理程序和SemaphoreSlim类字段来执行此操作。
private SemaphoreSlim signalDownloadComplete = new SemaphoreSlim(0, 1);
private bool isDownloading = false;
//Add to DownloadAsync() method
myWebClient.DownloadDataCompleted += (s, e) => {
isDownloading = false;
signalDownloadComplete.Release();
}
isDownloading = true;
//Add to block main calling method from returning until download is completed
if (isDownloading)
{
await signalDownloadComplete.WaitAsync();
}
我也遇到过类似的问题,但通过将CancellationToken作为参数传递给异步方法来解决。
带附件的电子邮件通知示例
public async Task SendNotification(string SendTo,string[] cc,string subject,string body,string path)
{
SmtpClient client = new SmtpClient();
MailMessage message = new MailMessage();
message.To.Add(new MailAddress(SendTo));
foreach (string ccmail in cc)
{
message.CC.Add(new MailAddress(ccmail));
}
message.Subject = subject;
message.Body =body;
message.Attachments.Add(new Attachment(path));
//message.Attachments.Add(a);
try {
message.Priority = MailPriority.High;
message.IsBodyHtml = true;
await Task.Yield();
client.Send(message);
}
catch(Exception ex)
{
ex.ToString();
}
}