Подтвердить что ты не робот

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 внутри конструктора контроллера?

4b9b3361

Ответ 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();
        }
 }