ASP.NET контроллер: асинхронный модуль или обработчик завершен, пока асинхронная операция еще не завершена
у меня очень простой 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: асинхронный модуль или обработчик завершен, пока асинхронная операция еще не завершена.]
почему это происходит? Я сделал пару тестов:--7-->
- если убрать
task = DownloadAsync();
из конструктора и положите его вIndex
метод, он будет работать нормально без ошибок. - если мы используем другой
DownloadAsync()
телоreturn await Task.Factory.StartNew(() => { Thread.Sleep(3000); return "Give me an error"; });
он будет работать должным образом.
почему нельзя использовать WebClient.DownloadStringTaskAsync
метод внутри конструктора контроллера?
6 ответов
на асинхронный Void, ASP.Net, и количество выдающихся операций Стефан Клири объясняет корень этой ошибки:
исторически, ASP.NET поддерживает чистые асинхронные операции начиная с .NET 2.0 через асинхронный шаблон на основе событий (EAP), in какие асинхронные компоненты уведомляют 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
. Обратите внимание, что при этом вы используете поток threadpool асинхронные операции ввода-вывода, что является избыточным и именно то, что асинхронный ввод-вывод с async-await
попытки преодолеть.
Я столкнулся с связанной проблемой. Клиент использует интерфейс, который возвращает задачу и реализуется с помощью async.
в Visual Studio 2015 клиентский метод, который является асинхронным и который не использует ключевое слово await при вызове метода не получает предупреждения или ошибки, код компилируется чисто. Условие гонки повышено к продукции.
ASP.NET считает незаконным запуск "асинхронной операции", привязанной к его SynchronizationContext
и возврат ActionResult
до завершения всех начатых операций. Все!--3--> методы регистрируются как "асинхронные операции", поэтому вы должны убедиться, что все такие вызовы, которые привязываются к ASP.NET SynchronizationContext
завершить до возвращения ActionResult
.
в вашем коде вы возвращаетесь, не гарантируя, что DownloadAsync()
полностью завершена. Тем не менее, вы сохраняете результат в task
член, поэтому обеспечение того, чтобы это было завершено, очень легко. Проще говоря await task
во всех ваших методов действий (после asyncifying них) до возвращения:
public async Task<ActionResult> IndexAsync()
{
try
{
return View();
}
finally
{
await task;
}
}
EDIT:
в некоторых случаях вам может потребоваться вызвать async
метод не следует завершать до возвращения в ASP.NET. Например, может потребоваться лениво инициализировать фоновую задачу службы, которая должна выполняться после завершения текущего запроса. Это не так. код в ОП, потому что ОП хочет, чтобы до возвращения. Однако, если вам нужно начать и не ждать задачи, есть способ сделать это. Вы просто должны использовать технику, чтобы "убежать" от текущей SynchronizationContext.Current
.
(не возобновилась) одна особенность
Task.Run()
, чтобы избежать текущего контекста синхронизации. Однако люди рекомендуют не использовать это в ASP.NET потому что ASP.NET тредпул особенный. Кроме того, даже снаружи ASP.NET, этот подход приводит к дополнительному переключению контекста.(рекомендовано) безопасный способ избежать текущего контекста синхронизации, не заставляя дополнительный переключатель контекста или беспокоить ASP.NET ' s threadpool немедленно является set
SynchronizationContext.Current
tonull
, вызов вашейasync
метод, а затем восстановить исходное значение.
метод myWebClient.DownloadStringTaskAsync работает в отдельном потоке и не блокируется. Возможное решение-сделать это с помощью обработчика событий DownloadDataCompleted для myWebClient и поля класса 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();
}
метод return async Task
и ConfigureAwait(false)
может быть одним из решений. Он будет действовать как async void и не продолжать контекст синхронизации (если вы действительно не касаетесь конечного результата метода)
пример уведомления по электронной почте с вложением ..
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();
}
}