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

Async Void, ASP.Net и количество неиспользуемых операций

Я пытаюсь понять, почему метод async void в приложении ASP.Net может привести к следующему исключению, в то время как кажется, что async Task не будет:

System.InvalidOperationException: An asynchronous module or handler 
completed while an asynchronous operation was still pending

Я относительно новичок в мире async в .NET, но чувствую, что я попытался запустить его с помощью нескольких существующих ресурсов, включая все следующие:

Из этих ресурсов я понимаю, что наилучшей практикой является, как правило, возврат задачи и исключение async void. Я также понимаю, что async void увеличивает количество выдающихся операций при вызове метода и уменьшает его при завершении. Это звучит, по крайней мере, как часть ответа на мой вопрос. Однако то, что мне не хватает, это то, что происходит, когда я возвращаю Task и почему это делает вещи "работающими".

Вот надуманный пример, который еще раз иллюстрирует мой вопрос:

public class HomeController : AsyncController
{
    // This method will work fine
    public async Task<ActionResult> ThisPageWillLoad()
    {
        // Do not await the task since it is meant to be fire and forget
        var task = this.FireAndForgetTask();

        return await Task.FromResult(this.View("Index"));
    }

    private async Task FireAndForgetTask()
    {
        var task = Task.Delay(TimeSpan.FromSeconds(3));
        await task;
    }

    // This method will throw the following exception:
    // System.InvalidOperationException: An asynchronous module or 
    // handler completed while an asynchronous operation was still pending
    public async Task<ActionResult> ThisPageWillNotLoad()
    {
        // Obviously can't await a void method
        this.FireAndForgetVoid();

        return await Task.FromResult(this.View("Index"));
    }

    private async void FireAndForgetVoid()
    {
        var task = Task.Delay(TimeSpan.FromSeconds(3));
        await task;
    }
}

В отношении связанного примечания, если мое понимание асинхронного права правильное, тогда не так ли неправильно думать о async void как о "пожаре и забыть" в этом сценарии, поскольку ASP.Net на самом деле не забывает об этом?

4b9b3361

Ответ 1

Microsoft приняла решение избежать как можно большего количества проблем с совместимостью при переносе async в ASP.NET. И они хотели довести его до всех своих "один ASP.NET" - поддержка async для WinForms, MVC, WebAPI, SignalR и т.д.

Исторически ASP.NET поддерживал чистые асинхронные операции с .NET 2.0 через асинхронный шаблон на основе событий (EAP), в котором асинхронные компоненты уведомляют SynchronizationContext об их запуске и завершении..NET 4.5 приносит первые довольно значительные изменения в эту поддержку, обновляя основные асинхронные типы ASP.NET, чтобы лучше включить асинхронный шаблон на основе задач (TAP, т.е. async).

Тем временем, каждая другая структура (WebForms, MVC и т.д.) разработала свой собственный способ взаимодействия с этим ядром, сохраняя обратную совместимость приоритетом. В попытке помочь разработчикам основной ASP.NET SynchronizationContext был расширен за исключением того, что вы видите; он поймает много ошибок использования.

В мире WebForms они имеют RegisterAsyncTask, но многие люди просто используют обработчики событий async void. Таким образом, ASP.NET SynchronizationContext позволит async void в нужное время в течение жизненного цикла страницы, и если вы используете его в неподходящее время, это приведет к возникновению этого исключения.

В мире MVC/WebAPI/SignalR структуры более структурированы как сервисы. Таким образом, они смогли принять async Task очень естественным образом, и рамочная структура должна иметь дело с возвращенной Task - очень чистой абстракцией. В качестве дополнительной заметки вам больше не нужно AsyncController; MVC знает, что он асинхронен только потому, что возвращает Task.

Однако, если вы попытаетесь вернуть Task и используйте async void, который не поддерживается. И нет оснований для его поддержки; было бы довольно сложно просто поддерживать пользователей, которые в любом случае не должны этого делать. Помните, что async void напрямую ссылается на основной ASP.NET SynchronizationContext, полностью обходя рамки MVC. Структура MVC понимает, как ждать вашего Task, но он даже не знает о async void, поэтому он возвращает завершение в ядро ​​ASP.NET, которое видит, что оно действительно не завершено.

Это может вызвать проблемы в двух сценариях:

  • Вы пытаетесь использовать некоторую библиотеку или еще что-то, что использует async void. Извините, но очевидным фактом является то, что библиотека сломана и должна быть исправлена.
  • Вы переносите компонент EAP в Task и правильно используете await. Это может вызвать проблемы, поскольку компонент EAP напрямую взаимодействует с SynchronizationContext. В этом случае лучшим решением является изменение типа, чтобы он поддерживал TAP естественным образом или заменял его типом TAP (например, HttpClient вместо WebClient). В противном случае вы можете использовать TAP-over-APM вместо TAP-over-EAP. Если ни один из этих вариантов невозможен, вы можете просто использовать Task.Run вокруг своей обертки TAP-over-EAP.

Относительно "огня и забыть":

Я лично никогда не использую эту фразу для методов async void. С одной стороны, семантика обработки ошибок, безусловно, не соответствует фразе "огонь и забыть" ; Я в шутку ссылаюсь на методы async void как "огонь и авария". Истинный async метод "огонь и забыть" будет async Task, где вы игнорируете возвращенный Task, а не ожидаете его.

Тем не менее, в ASP.NET вы почти никогда не хотите возвращаться с ранних запросов (что и подразумевает "огонь и забыть" ). Этот ответ уже слишком длинный, но у меня есть описание проблем в моем блоге, а также некоторый код для поддержки ASP.NET "огонь и забыть" , если это действительно необходимо.