Поделиться через


Использование асинхронных методов в ASP.NET MVC 4

Рик Андерсон

В этом руководстве вы узнаете об основах создания асинхронного веб-приложения ASP.NET MVC с помощью Visual Studio Express 2012 for Web, которая является бесплатной версией Microsoft Visual Studio. Вы также можете использовать Visual Studio 2012.

Полный пример приведен для этого руководства на сайте GitHub. https://github.com/RickAndMSFT/Async-ASP.NET/

Класс контроллера ASP.NET MVC 4 в сочетании .NET 4.5 позволяет создавать асинхронные методы действий, возвращающие объект типа Task<ActionResult>. В платформа .NET Framework 4 появилась концепция асинхронного программирования, называемая задачей, и ASP.NET MVC 4 поддерживает task. Задачи представлены типом задачи и связанными типами в пространстве имен System.Threading.Tasks . Платформа .NET Framework 4.5 основан на этой асинхронной поддержке с ключевыми словами await и async, которые делают работу с объектами Task гораздо менее сложной, чем предыдущие асинхронные подходы. Ключевое слово ожидания является синтаксическим сокращением, указывающим, что фрагмент кода должен асинхронно ожидать какой-то другой фрагмент кода. Асинхронный ключевое слово представляет подсказку, которую можно использовать для пометки методов как асинхронных методов на основе задач. Сочетание объектов await, async и Task значительно упрощает написание асинхронного кода в .NET 4.5. Новая модель для асинхронных методов называется асинхронной моделью на основе задач (TAP). В этом руководстве предполагается, что вы знакомы с асинхронным программированием с использованием ключевых слов await и async , а также пространства имен Задачи .

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

Порядок обработки запросов пулом потоков

На веб-сервере платформа .NET Framework поддерживает пул потоков, которые используются для обслуживания ASP.NET запросов. При получении запроса, для его обработки из этого пула выделяется поток. Если запрос обрабатывается синхронно, поток, обрабатывающий запрос, занят во время обработки запроса, и этот поток не может обслуживать другой запрос.

Это не может быть проблемой, так как пул потоков может быть достаточно большим, чтобы вместить много занятых потоков. Однако количество потоков в пуле потоков ограничено (максимальное значение по умолчанию для .NET 4.5 — 5000). В больших приложениях с высоким параллелизмом длительных запросов все доступные потоки могут быть заняты. Такая ситуация называется нехваткой потоков. При достижении этого условия веб-сервер помещает запросы в очередь. Если очередь запросов заполняется, веб-сервер отклоняет запросы с состоянием HTTP 503 (Сервер слишком занят). Пул потоков CLR имеет ограничения на внедрение новых потоков. Если параллелизм является высокой (то есть веб-сайт может внезапно получить большое количество запросов) и все доступные потоки запросов заняты из-за внутренних вызовов с высокой задержкой, ограниченная скорость внедрения потоков может заставить приложение реагировать очень плохо. Кроме того, каждый новый поток, добавленный в пул потоков, имеет накладные расходы (например, 1 МБ памяти стека). Веб-приложение, использующее синхронные методы для обслуживания вызовов с высокой задержкой, когда пул потоков увеличивается до максимального значения по умолчанию .NET 4.5 в 5000 потоков, будет потреблять примерно на 5 ГБ памяти больше, чем приложение может обслуживать те же запросы, используя асинхронные методы и только 50 потоков. При выполнении асинхронной работы вы не всегда используете поток. Например, при выполнении асинхронного запроса веб-службы ASP.NET не будут использовать потоки между вызовом асинхронного метода и await. Использование пула потоков для запросов на обслуживание с высокой задержкой может привести к увеличению объема памяти и плохому использованию серверного оборудования.

Обработка асинхронных запросов

В веб-приложении, которое видит большое количество одновременных запросов при запуске или имеет высокую нагрузку (где параллелизм внезапно увеличивается), асинхронные вызовы веб-служб повышают скорость реагирования приложения. Асинхронный запрос обрабатывается такое же количество времени, что и синхронный запрос. Если запрос выполняет вызов веб-службы, который требует двух секунд, запрос занимает две секунды независимо от того, выполняется ли он синхронно или асинхронно. Однако во время асинхронного вызова поток не блокируется от ответа на другие запросы, пока ожидает завершения первого запроса. Таким образом, асинхронные запросы предотвращают создание очередей запросов и увеличение пула потоков при наличии большого количества параллельных запросов, вызывающих длительные операции.

Выбор синхронного или асинхронного метода действия

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

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

  • Операции являются простыми и кратковременными.
  • Простота имеет большое значение, чем эффективность.
  • Операции в основном являются операциями ЦП, а не операциями, в которых широко используется диск или сетевые ресурсы. Использование асинхронных методов действия в операциях, которые связаны с ЦП, не дает преимущества и приводит к дополнительному расходу ресурсов.

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

  • Вы вызываете службы, которые можно использовать с помощью асинхронных методов, и используете .NET 4.5 или более поздней версии.
  • Операции связаны с использованием сети или ввода-вывода, а не с ЦП.
  • Параллелизм имеет большее значение, чем простота кода.
  • Необходимо предоставить механизм, который позволяет пользователям отменить длительные по времени запросы.
  • Когда преимущество переключения потоков перевешивает стоимость переключения контекста. Как правило, следует сделать метод асинхронным, если синхронный метод ожидает в потоке запроса ASP.NET, не выполняя никаких действий. Выполняя асинхронный вызов, поток запроса ASP.NET не зависает, не выполняя никаких действий, пока ожидает завершения запроса веб-службы.
  • Тестирование показывает, что операции блокировки являются узким местом в производительности сайта и что СЛУЖБЫ IIS могут обслуживать больше запросов, используя асинхронные методы для этих блокирующих вызовов.

В примере показано, как эффективно использовать асинхронные методы действия. Предоставленный пример был разработан для простой демонстрации асинхронного программирования в ASP.NET MVC 4 с использованием .NET 4.5. Пример не является эталонной архитектурой для асинхронного программирования в ASP.NET MVC. Пример программы вызывает веб-API ASP.NET методы, которые, в свою очередь, вызывают Task.Delay для имитации длительных вызовов веб-служб. Большинство рабочих приложений не будут показывать таких очевидных преимуществ при использовании асинхронных методов действий.

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

Пример приложения

Пример приложения https://github.com/RickAndMSFT/Async-ASP.NET/ можно скачать на сайте GitHub . Репозиторий состоит из трех проектов:

  • Mvc4Async: проект ASP.NET MVC 4, содержащий код, используемый в этом руководстве. Он выполняет вызовы веб-API к службе WebAPIpgw .
  • WebAPIpgw: проект веб-API ASP.NET MVC 4, реализующий Products, Gizmos and Widgets контроллеры. Он предоставляет данные для проектов WebAppAsync и Mvc4Async .
  • WebAppAsync: проект ASP.NET Web Forms, используемый в другом руководстве.

Метод синхронного действия Gizmos

В следующем коде Gizmos показан метод синхронного действия, который используется для отображения списка gizmos. (В этой статье gizmo является вымышленным механическим устройством.)

public ActionResult Gizmos()
{
    ViewBag.SyncOrAsync = "Synchronous";
    var gizmoService = new GizmoService();
    return View("Gizmos", gizmoService.GetGizmos());
}

В следующем коде GetGizmos показан метод службы gizmo.

public class GizmoService
{
    public async Task<List<Gizmo>> GetGizmosAsync(
        // Implementation removed.
       
    public List<Gizmo> GetGizmos()
    {
        var uri = Util.getServiceUri("Gizmos");
        using (WebClient webClient = new WebClient())
        {
            return JsonConvert.DeserializeObject<List<Gizmo>>(
                webClient.DownloadString(uri)
            );
        }
    }
}

Метод GizmoService GetGizmos передает универсальный код ресурса (URI) веб-API ASP.NET службе HTTP, которая возвращает список данных gizmos. Проект WebAPIpgw содержит реализацию веб-API gizmos, widget и product контроллеров.
На следующем рисунке показано представление gizmos из примера проекта.

Гизмо

Создание метода асинхронного действия gizmos

В примере используются новые ключевые слова async и await (доступные в .NET 4.5 и Visual Studio 2012), чтобы компилятор отвечал за обслуживание сложных преобразований, необходимых для асинхронного программирования. Компилятор позволяет писать код с помощью синхронных конструкций потока управления C#, а компилятор автоматически применяет преобразования, необходимые для использования обратных вызовов, чтобы избежать блокировки потоков.

В следующем коде Gizmos показаны синхронный метод и GizmosAsync асинхронный метод. Если браузер поддерживает элемент HTML 5<mark>, вы увидите, что изменения будут GizmosAsync выделены желтым цветом.

public ActionResult Gizmos()
{
    ViewBag.SyncOrAsync = "Synchronous";
    var gizmoService = new GizmoService();
    return View("Gizmos", gizmoService.GetGizmos());
}
public async Task<ActionResult> GizmosAsync()
{
    ViewBag.SyncOrAsync = "Asynchronous";
    var gizmoService = new GizmoService();
    return View("Gizmos", await gizmoService.GetGizmosAsync());
}

Следующие изменения были применены, чтобы сделать GizmosAsync асинхронным.

  • Метод помечен асинхронным ключевое слово, который указывает компилятору создать обратные вызовы для частей тела и автоматически создать Task<ActionResult> возвращаемый объект .
  • "Async" добавлен к имени метода. Добавление "Async" не является обязательным, но является соглашением при написании асинхронных методов.
  • Тип возвращаемого значения изменен с ActionResult на Task<ActionResult>. Тип возвращаемого Task<ActionResult> значения представляет текущую работу и предоставляет вызывающим методам дескриптор, с помощью которого ожидается завершение асинхронной операции. В этом случае вызывающим является веб-служба. Task<ActionResult> представляет текущую работу с результатом ActionResult.
  • Ключевое слово ожидания был применен к вызову веб-службы.
  • Асинхронный API веб-службы был вызван (GetGizmosAsync).

GetGizmosAsync Внутри тела метода вызывается другой асинхронный методGetGizmosAsync. GetGizmosAsync немедленно возвращает объект , Task<List<Gizmo>> который в конечном итоге завершится, когда данные будут доступны. Так как вы не хотите ничего делать, пока не будете получать данные gizmo, код ожидает задачу (используя ключевое слово await). Вы можете использовать ключевое слово await только в методах, помеченных асинхронным ключевое слово.

Ключевое слово await не блокирует поток до завершения задачи. Он регистрирует остальную часть метода в качестве обратного вызова задачи и немедленно возвращается. Когда ожидаемая задача в конечном итоге завершится, она вызовет этот обратный вызов и, таким образом, возобновит выполнение метода прямо с того места, где он закончился. Дополнительные сведения об использовании ключевых слов await и async , а также пространства имен задач см. в справочниках по асинхронным действиям.

В следующем коде приведены методы GetGizmos и GetGizmosAsync.

public List<Gizmo> GetGizmos()
{
    var uri = Util.getServiceUri("Gizmos");
    using (WebClient webClient = new WebClient())
    {
        return JsonConvert.DeserializeObject<List<Gizmo>>(
            webClient.DownloadString(uri)
        );
    }
}
public async Task<List<Gizmo>> GetGizmosAsync()
{
    var uri = Util.getServiceUri("Gizmos");
    using (HttpClient httpClient = new HttpClient())
    {
        var response = await httpClient.GetAsync(uri);
        return (await response.Content.ReadAsAsync<List<Gizmo>>());
    }
}

Асинхронные изменения похожи на изменения, внесенные в GizmosAsync выше.

  • Сигнатура метода была помечена асинхронным ключевое слово, тип возвращаемого значения был изменен на Task<List<Gizmo>>, а к имени метода добавлен асинхронный.
  • Вместо класса WebClient используется асинхронный класс HttpClient .
  • Ключевое слово await был применен к асинхронным методам HttpClient.

На следующем рисунке показано асинхронное представление gizmo.

Async

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

Параллельное выполнение нескольких операций

Асинхронные методы действий имеют значительное преимущество по сравнению с синхронными методами, когда действие должно выполнять несколько независимых операций. В приведенном примере синхронный метод PWG(для Продуктов, Мини-приложений и Gizmos) отображает результаты трех вызовов веб-служб для получения списка продуктов, мини-приложений и gizmos. Проект веб-API ASP.NET, предоставляющий эти службы, использует Task.Delay для имитации задержки или медленных сетевых вызовов. Если для задержки задано значение 500 миллисекунда, асинхронный PWGasync метод занимает чуть более 500 миллисекунда, а синхронная PWG версия — более 1500 миллисекунда. Синхронный PWG метод показан в следующем коде.

public ActionResult PWG()
{
    ViewBag.SyncType = "Synchronous";
    var widgetService = new WidgetService();
    var prodService = new ProductService();
    var gizmoService = new GizmoService();

    var pwgVM = new ProdGizWidgetVM(
        widgetService.GetWidgets(),
        prodService.GetProducts(),
        gizmoService.GetGizmos()
       );

    return View("PWG", pwgVM);
}

Асинхронный PWGasync метод показан в следующем коде.

public async Task<ActionResult> PWGasync()
{
    ViewBag.SyncType = "Asynchronous";
    var widgetService = new WidgetService();
    var prodService = new ProductService();
    var gizmoService = new GizmoService();

    var widgetTask = widgetService.GetWidgetsAsync();
    var prodTask = prodService.GetProductsAsync();
    var gizmoTask = gizmoService.GetGizmosAsync();

    await Task.WhenAll(widgetTask, prodTask, gizmoTask);

    var pwgVM = new ProdGizWidgetVM(
       widgetTask.Result,
       prodTask.Result,
       gizmoTask.Result
       );

    return View("PWG", pwgVM);
}

На следующем рисунке показано представление, возвращенное методом PWGasync .

pwgAsync

Использование маркера отмены

Асинхронные методы действий, возвращающие Task<ActionResult>, могут быть отменены, то есть они принимают параметр CancellationToken , если он предоставляется с атрибутом AsyncTimeout . В следующем коде GizmosCancelAsync показан метод с временем ожидания 150 миллисекундами.

[AsyncTimeout(150)]
[HandleError(ExceptionType = typeof(TimeoutException),
                                    View = "TimeoutError")]
public async Task<ActionResult> GizmosCancelAsync(
                       CancellationToken cancellationToken )
{
    ViewBag.SyncOrAsync = "Asynchronous";
    var gizmoService = new GizmoService();
    return View("Gizmos",
        await gizmoService.GetGizmosAsync(cancellationToken));
}

В следующем коде показана перегрузка GetGizmosAsync, которая принимает параметр CancellationToken .

public async Task<List<Gizmo>> GetGizmosAsync(string uri,
    CancellationToken cancelToken = default(CancellationToken))
{
    using (HttpClient httpClient = new HttpClient())
    {
        var response = await httpClient.GetAsync(uri, cancelToken);
        return (await response.Content.ReadAsAsync<List<Gizmo>>());
    }
}

В предоставленном примере приложения при выборе ссылки Демонстрация токена отмены вызывается GizmosCancelAsync метод и демонстрируется отмена асинхронного вызова.

Конфигурация сервера для вызовов веб-служб с высокой степенью параллелизма и высокой задержкой

Чтобы реализовать преимущества асинхронного веб-приложения, может потребоваться внести некоторые изменения в конфигурацию сервера по умолчанию. При настройке и стресс-тестировании асинхронного веб-приложения учитывайте следующее.

  • Windows 7, Windows Vista и все клиентские операционные системы Windows имеют не более 10 одновременных запросов. Вам потребуется операционная система Windows Server, чтобы увидеть преимущества асинхронных методов при высокой нагрузке.

  • Зарегистрируйте .NET 4.5 в IIS из командной строки с повышенными привилегиями:
    %windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_regiis -i
    См . ASP.NET средство регистрации IIS (Aspnet_regiis.exe)

  • Может потребоваться увеличить ограничение HTTP.sys очереди с 1000 до 5000 по умолчанию. Если параметр слишком низкий, вы можете увидеть ,HTTP.sys отклонять запросы с состоянием HTTP 503. Чтобы изменить ограничение HTTP.sys очереди, выполните следующие действия.

    • Откройте диспетчер IIS и перейдите в область Пулы приложений.
    • Щелкните правой кнопкой мыши целевой пул приложений и выберите Дополнительные параметры.
      Дополнительные
    • В диалоговом окне Дополнительные параметры измените значение Параметра Длина очереди с 1000 на 5000.
      Длина очереди

    Обратите внимание, что на приведенных выше изображениях платформа .NET framework указана как версия 4.0, даже если пул приложений использует .NET 4.5. Чтобы понять это несоответствие, ознакомьтесь со следующими статьями:

  • Если приложение использует веб-службы или System.NET для взаимодействия с серверной частью по протоколу HTTP, может потребоваться увеличить элемент connectionManagement/maxconnection . Для ASP.NET приложений эта возможность ограничена функцией автонастройки в 12 раз больше ЦП. Это означает, что на quad-proc вы можете иметь не более 12 * 4 = 48 одновременных подключений к конечной точке IP. Так как это связано с autoConfig, самый простой способ увеличить maxconnection ASP.NET приложения — задать System.Net.ServicePointManager.DefaultConnectionLimit программным способом в методе from Application_Start в файле global.asax . Пример см. в примере загрузки.

  • В .NET 4.5 значение по умолчанию 5000 для MaxConcurrentRequestsPerCPU должно быть в порядке.