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: асинхронный модуль или обработчик завершен, пока асинхронная операция все еще выполняется.]
Почему это происходит? Я сделал пару тестов:
- Если мы удалим
task = DownloadAsync();
из конструктора и поместим его в метод Index
, он будет работать без ошибок.
- Если мы используем другое тело
DownloadAsync()
body return await Task.Factory.StartNew(() => { Thread.Sleep(3000); return "Give me an error"; });
, он будет работать правильно.
Почему невозможно использовать метод WebClient.DownloadStringTaskAsync
внутри конструктора контроллера?
Ответы
Ответ 1
В Async Void, ASP.Net и Count of Outstanding Operations, Стефан Клири объясняет корень этой ошибки:
Исторически ASP.NET поддерживал чистые асинхронные операции поскольку .NET 2.0 через асинхронный шаблон на основе событий (EAP), в которые асинхронные компоненты уведомляют SynchronizationContext их запуск и завершение.
Что происходит, так это то, что вы запускаете DownloadAsync
внутри своего конструктора классов, где внутри вас await
выполняется вызов async http. Это регистрирует асинхронную операцию с ASP.NET SynchronizationContext
. Когда ваш HomeController
возвращается, он видит, что он ожидает ожидающую асинхронную операцию, которая еще не завершена, и именно поэтому она вызывает исключение.
Если мы удалим task = DownloadAsync(); от конструктора и поставить его в методе индекса он будет работать нормально без ошибок.
Как я объяснял выше, это потому, что у вас больше нет ожидающей асинхронной операции при возврате с контроллера.
Если мы используем другой экземпляр ReturnAsync() 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 IO с async-await
.
Ответ 2
У меня возникла проблема. Клиент использует интерфейс, который возвращает Task и реализуется с помощью async.
В Visual Studio 2015 клиентский метод, который является асинхронным и не использует ключевое слово await при вызове метода, не получает никаких предупреждений или ошибок, код компилируется чисто. Состояние гонки способствует производству.
Ответ 3
Метод 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();
}
Ответ 4
ASP.NET считает незаконным запуск "асинхронной операции", привязанной к ее SynchronizationContext
, и возврат ActionResult
до завершения всех начатых операций. Все методы async
регистрируются как "асинхронные операции", поэтому вы должны убедиться, что все такие вызовы, которые привязаны к ASP.NET SynchronizationContext
, завершены до возврата ActionResult
.
В вашем коде вы вернетесь, не убедившись, что DownloadAsync()
запустилось. Однако вы сохраняете результат в элементе task
, поэтому гарантировать, что это будет завершено, очень просто. Просто поместите await task
во все ваши действия (после их асинхронизации) до возврата:
public async Task<ActionResult> IndexAsync()
{
try
{
return View();
}
finally
{
await task;
}
}
EDIT:
В некоторых случаях вам может потребоваться вызвать метод async
, который не должен завершать до возврата в ASP.NET. Например, вам может потребоваться лениво инициализировать задачу фоновой службы, которая должна продолжаться после завершения текущего запроса. Это не относится к коду OPs, потому что OP хочет завершить задачу перед возвратом. Однако, если вам нужно начать и не ждать задания, есть способ сделать это. Вы просто должны использовать технику для "выхода" из текущего SynchronizationContext.Current
.
-
(не возобновлено). Одна из функций Task.Run()
заключается в том, чтобы избежать текущего контекста синхронизации. Тем не менее, люди рекомендуют не использовать это в ASP.NET, потому что ASP.NETs threadpool является особенным. Кроме того, даже вне ASP.NET этот подход приводит к дополнительному контекстному коммутатору.
-
(рекомендуется) Безопасный способ избежать текущего контекста синхронизации без принудительного дополнительного переключения контекста или беспокоиться о потоке потока ASP.NET в данный момент - установите SynchronizationContext.Current
в null
, вызовите ваш метод async
, а затем восстановите исходное значение.
Ответ 5
Возврат метода async Task
, а ConfigureAwait(false)
может быть одним из решений. Он будет действовать как async void и не продолжит контекст синхронизации (до тех пор, пока вы действительно не касаетесь конечного результата метода)
Ответ 6
Пример уведомления по электронной почте с приложением.
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();
}
}