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-->

  1. если убрать task = DownloadAsync(); из конструктора и положите его в Index метод, он будет работать нормально без ошибок.
  2. если мы используем другой 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 to null, вызов вашей 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();
        }
 }