我有一个非常简单的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
方法?
答案 0 :(得分:47)
在Async Void, ASP.Net, and Count of Outstanding Operations中,Stephan Cleary解释了此错误的根源:
从历史上看,ASP.NET支持干净的异步操作 从.NET 2.0开始,通过基于事件的异步模式(EAP), in 哪些异步组件通知SynchronizationContext 他们的开始和完成。
正在发生的事情是您在类构造函数中触发DownloadAsync
,在异步http调用中await
内部SynchronizationContext
。这将注册异步操作与ASP.NET HomeController
。当你的Task.Factory.StartNew(() => { Thread.Sleep(3000); return "Give me an
error"; });
返回时,它会看到它有一个尚未完成的挂起异步操作,这就是它引发异常的原因。
如果我们删除task = DownloadAsync();从构造函数并把它 进入Index方法,它将正常工作,没有错误。
正如我上面所解释的那样,因为从控制器返回时你不再有待处理的异步操作。
如果我们使用另一个DownloadAsync()体返回await
Task.Factory.StartNew
它会正常运作。
那是因为HostingEnvironment
在ASP.NET中做了一些危险的事情。它没有用ASP.NET注册任务执行。这可能导致执行池循环的边缘情况,完全忽略后台任务,导致异常中止。这就是为什么你必须使用注册任务的机制,例如HostingEnvironment.QueueBackgroundWorkItem
。
这就是为什么不能做你正在做的事情,以及你做这件事的方式。如果你真的希望这个在后台线程中执行,那就是“即兴完成”。样式,使用async-await
(如果您使用的是.NET 4.5.2)或BackgroundTaskManager
。请注意,通过执行此操作,您将使用线程池线程执行异步IO操作,这是多余的,并且正是{{1}}尝试克服的异步IO。
答案 1 :(得分:3)
ASP.NET认为启动绑定到其 $i_header["header"] = "PayPal-Mock-Response";
$i_header["value"] = json_encode(["mock_application_codes" => "INSTRUMENT_DECLINED"]);
$o_apiContext->addRequestHeader($i_header["header"], $i_header["value"]);
的“异步操作”并在所有已启动的操作完成之前返回SynchronizationContext
是非法的。所有ActionResult
方法都将自己注册为“异步操作”,因此您必须确保在返回async
之前完成绑定到ASP.NET SynchronizationContext
的所有此类调用。
在您的代码中,您无需确保ActionResult
已完成运行即可返回。但是,您将结果保存到DownloadAsync()
成员,因此确保完成此操作非常简单。在返回之前,只需将task
放入所有操作方法(在异步后):
await task
编辑:
在某些情况下,您可能需要调用{strong}在返回ASP.NET之前不应完成的public async Task<ActionResult> IndexAsync()
{
try
{
return View();
}
finally
{
await task;
}
}
方法。例如,您可能希望懒惰地初始化后台服务任务,该任务应在当前请求完成后继续运行。 OP的代码不是这种情况,因为OP希望在返回之前完成任务。但是,如果您确实需要启动而不是等待任务,则有一种方法可以执行此操作。您只需使用一种技术从当前async
“逃避”。
(未重新开始)SynchronizationContext.Current
的一个功能是转义当前的同步上下文。但是,人们建议不要在ASP.NET中使用它,因为ASP.NET的线程池很特殊。此外,即使在ASP.NET之外,这种方法也会导致额外的上下文切换。
(推荐)转义当前同步上下文而不强制进行额外的上下文切换或立即打扰ASP.NET的线程池的一种安全方法是set SynchronizationContext.Current
to null
, call your async
method, and then restore the original value。
答案 2 :(得分:2)
我遇到了相关的问题。客户端正在使用返回Task的接口,并使用async实现。
在Visual Studio 2015中,异步且在调用方法时不使用await关键字的客户端方法不会收到警告或错误,代码会干净地编译。竞争条件被提升为生产。
答案 3 :(得分:1)
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();
}
答案 4 :(得分:1)
方法返回async Task
,ConfigureAwait(false)
可以是解决方案之一。它会像async void一样,不会继续使用同步上下文(只要你真的不关心方法的最终结果)
答案 5 :(得分:0)
电子邮件通知示例附件..
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();
}
}
答案 6 :(得分:0)
我有一个类似的问题,但通过将CancellationToken作为参数传递给异步方法解决了。
答案 7 :(得分:0)
我今天在构建API控制器时遇到此错误。事实证明,就我而言,解决方案很简单。
我有:
public async void Post()
我需要将其更改为:
public async Task Post()
注意,编译器没有警告async void
。