Запуск и забвение асинхронной задачи в MVC Action

У меня есть стандартное неасинхронное действие, например:

[HttpPost]
public JsonResult StartGeneratePdf(int id)
{
    PdfGenerator.Current.GenerateAsync(id);
    return Json(null);
}

Идея в том, что я знаю, что это создание PDF может занять много времени, поэтому я просто запускаю задачу и возвращаюсь, не заботясь о результате асинхронной операции.

В приложении ASP.Net MVC 4 по умолчанию это дает мне это приятное исключение:

System.InvalidOperationException: в настоящее время невозможно запустить асинхронную операцию. Асинхронные операции можно запускать только в асинхронном обработчике или модуле или во время определенных событий в жизненном цикле страницы. Если это исключение возникло при выполнении Страницы, убедитесь, что Страница помечена как ‹%@ Page Async="true" %>.

Что совершенно не имеет отношения к моему сценарию. Глядя на это, я могу установить флаг в false, чтобы предотвратить это исключение:

<appSettings>
    <!-- Allows throwaway async operations from MVC Controller actions -->
    <add key="aspnet:AllowAsyncDuringSyncStages" value="true" />
</appSettings>

https://stackoverflow.com/a/15230973/176877
http://msdn.microsoft.com/en-us/library/hh975440.aspx

Но вопрос в том, есть ли вред, если запустить эту асинхронную операцию и забыть о ней из синхронного действия контроллера MVC? Все, что я могу найти, рекомендует сделать контроллер асинхронным, но это не то, что я ищу - в этом нет смысла, поскольку он всегда должен возвращаться немедленно.


person Chris Moschini    schedule 09.05.2013    source источник


Ответы (3)


Расслабьтесь, как говорит сама Microsoft (http://msdn.microsoft.com/en-us/library/system.web.httpcontext.allowasyncduringsyncstages.aspx):

Это поведение предназначено в качестве подстраховки, чтобы вы могли заранее узнать, пишете ли вы асинхронный код, который не соответствует ожидаемым шаблонам и может иметь негативные побочные эффекты.

Достаточно запомнить несколько простых правил:

  • Никогда не ожидайте внутри (асинхронных или нет) событий void (поскольку они возвращаются немедленно). Некоторые события WebForms Page поддерживают внутри себя простые ожидания, но RegisterAsyncTask по-прежнему является наиболее предпочтительным подходом.

  • Не ждите асинхронных методов void (поскольку они возвращаются немедленно).

  • Не ждите синхронно в графическом интерфейсе или потоке запросов (.Wait(), .Result(), .WaitAll(), WaitAny()) для асинхронных методов, внутри которых нет .ConfigureAwait(false) для root await, или их корень Task не запускается с .Run() или не имеет явно указанный TaskScheduler.Default (поскольку графический интерфейс или запрос, таким образом, заблокируются).

  • Используйте .ConfigureAwait(false) или Task.Run или явно укажите TaskScheduler.Default для каждого фонового процесса и в каждом библиотечном методе, который не должен продолжать контекст синхронизации — думайте об этом как о «вызывающем потоке», но знайте, что он не один (и не всегда на одном и том же), а может и вообще больше не существовать (если Запрос уже закончился). Это само по себе позволяет избежать наиболее распространенных ошибок async/await, а также повышает производительность.

Microsoft просто предположила, что вы забыли дождаться своей задачи...

ОБНОВЛЕНИЕ: Как Стивен ясно (каламбур) заявил в своем ответе, существует наследуемая, но скрытая опасность со всеми формами «выстрелил-забыл» при работе с пулами приложений, а не только для асинхронных /await, но и Tasks, ThreadPool и все другие подобные методы - они не гарантированно завершатся после завершения запроса (пул приложений может перезапуститься в любое время по ряду причин).

Вы можете заботиться об этом или нет (если это не критично для бизнеса, как в конкретном случае OP), но вы всегда должны знать об этом.

person Nikola Bogdanović    schedule 07.07.2013
comment
Поскольку на самом деле это предупреждение, к сожалению, это нельзя представить таким образом (например, как предупреждение для консоли вывода в режиме отладки). - person Chris Moschini; 07.07.2013
comment
Связанная документация также не очень хорошо хеджирует — они ссылаются на нее как: обнаруживает приложение, неправильно использующее асинхронный API. Возможно, это исключение нужно отделить от более коварной опасности, когда асинхронный модуль или обработчик сигнализирует о завершении еще незавершенной асинхронной работы. - person Chris Moschini; 07.07.2013
comment
да, очень жаль, что нет атрибута или какого-либо другого механизма, сигнализирующего о том, что в вашем случае это предполагаемое (и совершенно правильное) поведение, но, как и необработанные исключения задач, приводящие к сбою приложения в более ранней версии (лучше перестраховаться, чем сожалеть) , может быть, следующий тоже ослабит это требование... - person Nikola Bogdanović; 07.07.2013
comment
и даже эта коварная опасность только потенциально смертельна - если вам нужен HttpContext обратно, в остальном вполне безопасно - person Nikola Bogdanović; 07.07.2013
comment
Без сомнения, сообщение об исключении необходимо изменить, чтобы оно было менее жестким, говоря: «Если эта асинхронная задача предназначена для запуска и забвения, вы можете отключить это исключение, добавив aspnet:AllowAsyncDuringSyncStages в appSettings в Web.Config. Тогда это, по крайней мере, будет похоже на предупреждение, которым оно должно быть. - person Chris Moschini; 08.07.2013
comment
голосование против без объяснения причин вообще неконструктивно (особенно если вы, вероятно, ошибаетесь)... - person Nikola Bogdanović; 08.12.2013
comment
1) В документах, на которые вы ссылаетесь, указано, что код неправильно использует асинхронный API. 2) Вы не должны устанавливать опасный флаг AllowAsyncDuringSyncStages, если вы не можете описать негативные побочные эффекты, которые должны быть частью вашего ответа. 3) Ожидание внутри асинхронных событий void поддерживается ASP.NET, поэтому вам никогда не следует избегать первого. 4) Невозможно ожидать асинхронных методов void. 5) В этом сценарии нет потока GUI. 6) Ошибка (не предупреждение) не только потому, что оп забыл подождать; это потому, что они завершают запрос, когда есть еще не все работы. - person Stephen Cleary; 08.12.2013
comment
@StephenCleary: 1.) вы вырываете это из контекста, а моя цитата взята из более поздней, более актуальной части документа 2.) хорошо, да, но я указал все способы избежать их 3.) только некоторые из них, и только для простых задач, и это все еще не рекомендуется (hanselman.com/blog /) 4.) сейчас не помню, но думаю, что VS2010 с async/await CTP не предупредил вас об этом 5.) для полноты картины 6.) именно так, как он хотел - person Nikola Bogdanović; 08.12.2013
comment
Один из отрицательных побочных эффектов заключается в том, что контекст перестает быть действительным, когда метод async возвращает значение, как подробно описал Крис в своем ответе. Другой негативный побочный эффект заключается в том, что ваша дополнительная работа может просто никогда не завершиться. В вашем ответе даже не упоминаются эти проблемы, не говоря уже о способах их избежать/смягчить. - person Stephen Cleary; 08.12.2013
comment
@StephenCleary: эх... хорошо, я расширил свой третий пункт бюллетеня четвертым, чтобы полностью охватить один (хотя это всего лишь две стороны одной и той же проблемы синхронизации), и особо упомянул два (хотя OP никогда не заботился об этом) - теперь, пожалуйста, удалите отрицательный голос (или вы собираетесь придираться к чему-то еще)? - person Nikola Bogdanović; 09.12.2013

InvalidOperationException не является предупреждением. AllowAsyncDuringSyncStages — опасная настройка, которую я лично никогда не использовал.

правильное решение состоит в том, чтобы хранить запрос в постоянной очереди (например, в очереди Azure) и иметь отдельное приложение (например, рабочую роль Azure), обрабатывающее эту очередь. Это намного больше работы, но это правильный способ сделать это. Я имею в виду «правильно» в том смысле, что повторное использование IIS/ASP.NET вашего приложения не испортит вашу обработку.

Если вы абсолютно хотите, чтобы обработка выполнялась в памяти (и, как следствие, вы не против временной потери запросов), то по крайней мере зарегистрируйте работу с ASP.NET. У меня есть исходный код в моем блоге, который вы может зайти в ваше решение, чтобы сделать это. Но, пожалуйста, не просто хватайте код; пожалуйста, прочитайте весь пост, чтобы было ясно, почему это все еще не лучшее решение. :)

person Stephen Cleary    schedule 05.12.2013
comment
Просто чтобы уточнить, работа, которую я описываю в вопросе, несущественна - если AppDomain взорвется, сервер расплавится, что угодно, мне все равно. Либо я получил законченный PDF-файл, либо нет. Пользователь не задается вопросом, был ли он создан — есть отдельный вызов, который проверяет его статус. Для такого рода выброса я не согласен, что это опасно. - person Chris Moschini; 05.12.2013
comment
вы были бы совершенно правы, если бы ему нужно было вернуть какой-то ответ, но в его случае ему вообще не нужен контекст (вы прочитали весь вопрос) - так что оба ваших решения (совершенно действительные, как они есть в большинстве других случаев) здесь были бы просто ужасно раздутые иглы над головой... - person Nikola Bogdanović; 08.12.2013
comment
@NikolaBogdanovic: Вовсе нет; это не имеет никакого отношения к тому, нужно ли ему возвращать ответ; это связано с тем, нужна ли ему фоновая обработка, чтобы быть надежной. При размещении в IIS домены приложений и пулы приложений регулярно закрываются или перезапускаются при отсутствии активных запросов. Таким образом, мое первое решение является единственным правильным ответом, если дополнительная обработка (в данном случае создание PDF-файла) имеет решающее значение для бизнеса. И мое второе решение уведомляет ASP.NET о том, что происходит дополнительная обработка; он совсем не сильно раздут. - person Stephen Cleary; 08.12.2013
comment
@ChrisMoschini: В вашем случае вам не нужно первое решение. Однако я рекомендую вам использовать второе решение: исходный код из моего блога позволяет вам зарегистрировать фоновый код в ASP.NET, чтобы он знал об этом. - person Stephen Cleary; 08.12.2013
comment
@StephenCleary: вы, конечно, абсолютно правы, но он четко заявил, что это не критический для бизнеса сценарий. Кроме того, каждые 29 часов пулы приложений обновляются через 20 минут после последнего запроса, хотя на виртуальном хостинге это может происходить и происходит в любое время, так что и в этом случае вы будете совершенно правы. . Но для простого быстрого и грязного исправления это является совершенно правильным решением - и извините за ужасно раздутую часть, но оно отпугнуло бы любого нового приверженца шаблона async/await своей дополнительной сложностью. - person Nikola Bogdanović; 08.12.2013
comment
@NikolaBogdanović: я не согласен с отпугиванием async усыновителей; async в основном прост в использовании в ASP.NET, и я особенно рекомендую поддерживать активность сетей безопасности для новых пользователей. Это больше связано с попыткой использовать async для неправильной цели (возврат ответа, когда у вас еще есть работа), что я обычно не одобряю. У меня есть два сообщения в блоге 1, 2 с более подробным описанием. - person Stephen Cleary; 08.12.2013
comment
@StephenCleary Я не могу экспериментально проверить ваши утверждения о том, что ConfigureAwait достаточно для ее решения, поэтому я должен сказать, что настроен скептически, учитывая, что мое решение работает как в производственной, так и в локальной разработке, а ваше не дает полезных результатов - Я полностью открыт для доказательств, чтобы показать обратное, например, тестовый пример, демонстрирующий это на github. Я знаю о IRegisteredObject и считаю его неуместным в данном случае использования. Обычно для отключения AppDomain требуется около 30 секунд, и в любом случае это задание должно занимать меньше времени; даже если это не так, если он умрет, мне все равно. - person Chris Moschini; 12.12.2013
comment
@ChrisMoschini: я не совсем понимаю, что вы имеете в виду насчет ConfigureAwait; Я никогда не предлагал это как решение вашей проблемы. Я считаю, что IRegisteredObject здесь уместно; это специально для регистрации работы с ASP.NET, которая выполняется вне области запроса... что... ну... именно то, что вы делаете... - person Stephen Cleary; 12.12.2013
comment
@StephenCleary Согласен не согласиться; IRegisteredObject здесь — пустая трата усилий. - person Chris Moschini; 12.12.2013
comment
Наконец-то появилась возможность покопаться во всем этом подробнее. Если работа может безопасно умереть в любое время (идеальный способ написать любую фоновую работу в любом случае), в чем преимущество IRegisteredObject; есть ли какой-то вред, который я причиняю ASP.Net, если у меня есть задача, которая все еще работает, когда она решает снести себя? Чтобы быть ясным, меня не волнует, если Task умирает внезапно. Либо он завершает свою работу, что легко проверить, либо нет, что означает просто начать сначала. Таким образом, риск задачи в этом случае не имеет значения, но есть ли какой-то другой внешний системный риск, которого я не вижу? - person Chris Moschini; 10.05.2014
comment
Нет никакого вреда для ASP.NET; ваша задача прервет поток. Если у вас есть надежный способ обнаружить, что это произошло, то нет проблем с выполнением принципа «запустил и забыл». Регистрация просто дает вам более чистое завершение работы. Поскольку я опубликовал исходный ответ, я поместил код в библиотеку с устарело в .NET 4.5.2. Поэтому команда ASP.NET сочла, что этот метод достаточно полезен, чтобы включить его прямо в ядро. - person Stephen Cleary; 10.05.2014
comment
В предыдущих версиях ASP.Net уже было что-то похожее на HostingEnvironment.QueueBackgroundWorkItem — это HostingEnvironment.IncrementBusyCount, и вот моя простая реализация: Types/RegisteredTask.cs?at=master" rel="nofollow noreferrer">bitbucket.org/NikolaBogdanovic/utils/src/ - person Nikola Bogdanović; 09.08.2014

Ответ оказывается немного сложнее:

Если то, что вы делаете, как в моем примере, просто настраиваете длительную асинхронную задачу и возвращаетесь, вам не нужно делать больше, чем то, что я указал в своем вопросе.

Но есть риск: если кто-то расширит это действие позже, когда имело смысл сделать его асинхронным, то асинхронный метод «выстрели и забудь» внутри него будет случайным образом успешным или неудачным. Это выглядит так:

  1. Метод выстрелил и забыл.
  2. Поскольку он был запущен внутри асинхронной задачи, он попытается воссоединиться с контекстом этой задачи («упорядочить») при возвращении.
  3. Если асинхронное действие контроллера было завершено, а экземпляр контроллера с тех пор был удален сборщиком мусора, этот контекст задачи теперь будет нулевым.

Будет ли это на самом деле нуль, будет зависеть от вышеуказанного времени - иногда это так, иногда это не так. Это означает, что разработчик может протестировать и обнаружить, что все работает правильно, отправить в производство, и все взорвется. Хуже того, ошибка, которую это вызывает:

  1. NullReferenceException - очень расплывчато.
  2. Брошенный внутрь кода .Net Framework, вы даже не можете войти внутрь Visual Studio — обычно это System.Web.dll.
  3. Не перехватывается ни одной попыткой/поймать, потому что часть библиотеки параллельных задач, которая позволяет вам маршалировать обратно в существующие контексты попытки/поймать, является частью, которая дает сбой.

Таким образом, вы получите загадочную ошибку, когда ничего не происходит — исключения выбрасываются, но вы, вероятно, не осведомлены о них. Нехорошо.

Чистый способ предотвратить это:

[HttpPost]
public JsonResult StartGeneratePdf(int id)
{
    #pragma warning disable 4014 // Fire and forget.
    Task.Run(async () =>
    {
        await PdfGenerator.Current.GenerateAsync(id);
    }).ConfigureAwait(false);

    return Json(null);
}

Итак, здесь у нас есть синхронный контроллер без проблем, но чтобы гарантировать, что он по-прежнему не будет работать, даже если мы позже изменим его на асинхронный, мы явно запускаем новую задачу через Run, которая по умолчанию помещает задачу в основной ThreadPool. Если бы мы его ждали, он попытался бы связать его с этим контекстом, чего мы не хотим, поэтому мы его не ждем, и это дает нам неприятное предупреждение. Мы отключаем предупреждение с отключением предупреждения прагмы.

person Chris Moschini    schedule 04.12.2013
comment
вы правы, но это накладные расходы - просто убедитесь, что все ваши ожидания в PdfGenerator.Current.GenerateAsync не находятся в контексте синхронизации (ознакомьтесь с .ConfigureAwait(false)) - person Nikola Bogdanović; 08.12.2013
comment
вам нужно полностью понять мой третий пункт бюллетеня, прежде чем делать что-либо еще, связанное с async/await - у Стивена Клири ниже есть несколько очень хороших статей об этом... - person Nikola Bogdanović; 08.12.2013
comment
@NikolaBogdanović Вы проверяли это? Запуска с простым .ConfigureAwait(false) было недостаточно, чтобы предотвратить повторный вход Задачи в пустом контексте Задачи; выше было. Я не уверен, почему ваш третий пункт имеет здесь какое-либо значение, поскольку он касается потока графического интерфейса, а это действие контроллера ASP.Net. - person Chris Moschini; 08.12.2013
comment
GUI thread - это просто самый частый пример, я говорил о контексте синхронизации - где именно вы поставили .ConfigureAwait(false)? он должен быть в каждом корне await внутри вашего PdfGenerator.Current.GenerateAsync (используйте исходный код из вашего вопроса) - это точно так же, как вы окружаете его Task.Run или окружаете его вместо этого все тело (но с меньшим количеством иголок). На самом деле используйте .ConfigureAwait(false) для каждого фонового процесса или библиотеки, которым не нужно продолжать работу в вызывающем потоке, поскольку это увеличивает производительность. - person Nikola Bogdanović; 08.12.2013
comment
И ваш Task.Run способ также вводит замыкание на id - они не совсем надежны, и даже не рекомендуются в официальном документе об async/await, объясняющем, почему они добавили параметр объекта состояния в методы TaskFactory.StartNew() (но, как ни странно, не в Task.Run и PageAsyncTask, а также ???). Я не могу найти эту конкретную статью сейчас, но закрытие также снижает производительность: code.jonwagner.com/2012/09/06/best-practices-for-c-asyncawait - person Nikola Bogdanović; 08.12.2013
comment
...до сих пор не могу найти этот документ, но там говорится о Task.ContinueWith перегрузках вместо TaskFactory.StartNew - person Nikola Bogdanović; 08.12.2013
comment
Я обновил свой ответ четвертым пунктом бюллетеня - его несоблюдение является прямой причиной вашей ошибки контекста синхронизации (и моя третья ошибка бюллетеня - кстати, графический интерфейс и запрос полностью взаимозаменяемы) - person Nikola Bogdanović; 09.12.2013
comment
@NikolaBogdanović Наконец-то у меня появилась возможность проверить это - .ConfigureAwait(false) на самом деле необходим, как вы заметили. Обновленный ответ, чтобы отразить это. - person Chris Moschini; 12.04.2015