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

Рик Андерсон

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

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

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

Дополнительные сведения об использовании ключевых слов 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 4.5. Пример не является эталонной архитектурой для асинхронного программирования в ASP.NET. Пример программы вызывает методы веб-API ASP.NET, которые, в свою очередь, вызывают Task.Delay для имитации длительных вызовов веб-служб. Большинство рабочих приложений не будут показывать такие очевидные преимущества использования асинхронных методов.

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

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

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

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

Синхронная страница Gizmos

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

public partial class Gizmos : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        var gizmoService = new GizmoService();
        GizmoGridView.DataSource = gizmoService.GetGizmos();
        GizmoGridView.DataBind();
    }
}

В следующем коде 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) службе HTTP веб-API ASP.NET, которая возвращает список данных gizmos. Проект WebAPIpgw содержит реализацию веб-API gizmos, widget и product контроллеров.
На следующем рисунке показана страница gizmos из примера проекта.

Снимок экрана: страница веб-браузера Sync Gizmos с таблицей gizmos с соответствующими сведениями, введенными в контроллеры веб-API.

Создание асинхронной страницы gizmos

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

ASP.NET асинхронные страницы должны содержать директиву Page с атрибутом Async true. В следующем коде показана директива Page с атрибутом Async true для страницы GizmosAsync.aspx .

<%@ Page Async="true"  Language="C#" AutoEventWireup="true" 
    CodeBehind="GizmosAsync.aspx.cs" Inherits="WebAppAsync.GizmosAsync" %>

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

protected void Page_Load(object sender, EventArgs e)
{
   var gizmoService = new GizmoService();
   GizmoGridView.DataSource = gizmoService.GetGizmos();
   GizmoGridView.DataBind();
}

Асинхронная версия:

protected void Page_Load(object sender, EventArgs e)
{
    RegisterAsyncTask(new PageAsyncTask(GetGizmosSvcAsync));
}

private async Task GetGizmosSvcAsync()
{
    var gizmoService = new GizmoService();
    GizmosGridView.DataSource = await gizmoService.GetGizmosAsync();
    GizmosGridView.DataBind();
}

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

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

GetGizmosSvcAsync Внутри тела метода вызывается другой асинхронный метод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 (WebClient webClient = new WebClient())
    {
        return JsonConvert.DeserializeObject<List<Gizmo>>(
            await webClient.DownloadStringTaskAsync(uri)
        );
    }
}

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

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

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

Снимок экрана: страница веб-браузера Gizmos Async с таблицей gizmos с соответствующими сведениями, введенными в контроллеры веб-API.

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

Примечания RegisterAsyncTask

Методы, которые были подключены к RegisterAsyncTask , будут выполняться сразу после предварительной отрисовки.

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

protected async void Page_Load(object sender, EventArgs e) {
    await ...;
    // do work
}

вы больше не имеете полного контроля над выполнением событий. Например, если и ASPX, и . Главное определяет Page_Load события, и один или оба из них являются асинхронными, порядок выполнения не может быть гарантирован. Применяется тот же неопределенный порядок для обработчиков событий (например, async void Button_Click ).

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

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

protected void Page_Load(object sender, EventArgs e)
{
    Stopwatch stopWatch = new Stopwatch();
    stopWatch.Start();

    var widgetService = new WidgetService();
    var prodService = new ProductService();
    var gizmoService = new GizmoService();

    var pwgVM = new ProdGizWidgetVM(
        widgetService.GetWidgets(),
        prodService.GetProducts(),
        gizmoService.GetGizmos()
       );
    WidgetGridView.DataSource = pwgVM.widgetList;
    WidgetGridView.DataBind();
    ProductGridView.DataSource = pwgVM.prodList;
    ProductGridView.DataBind();
    GizmoGridView.DataSource = pwgVM.gizmoList;
    GizmoGridView.DataBind();

    stopWatch.Stop();
    ElapsedTimeLabel.Text = String.Format("Elapsed time: {0}", 
        stopWatch.Elapsed.Milliseconds / 1000.0);
}

Ниже показан асинхронный PWGasync код программной части.

protected void Page_Load(object sender, EventArgs e)
{
    Stopwatch stopWatch = new Stopwatch();
    stopWatch.Start();
    RegisterAsyncTask(new PageAsyncTask(GetPWGsrvAsync));
    stopWatch.Stop();
    ElapsedTimeLabel.Text = String.Format("Elapsed time: {0}",
        stopWatch.Elapsed.Milliseconds / 1000.0);
}

private async Task GetPWGsrvAsync()
{
    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
       );

    WidgetGridView.DataSource = pwgVM.widgetList;
    WidgetGridView.DataBind();
    ProductGridView.DataSource = pwgVM.prodList;
    ProductGridView.DataBind();
    GizmoGridView.DataSource = pwgVM.gizmoList;
    GizmoGridView.DataBind();           
}

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

Снимок экрана: страница веб-браузера

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

Асинхронные методы, возвращающие Taskметоды, можно отменить, то есть они принимают параметр CancellationToken , если он предоставляется с AsyncTimeout атрибутом директивы Page . В следующем коде показана страница GizmosCancelAsync.aspx со временем ожидания секунды.

<%@ Page  Async="true"  AsyncTimeout="1" 
    Language="C#" AutoEventWireup="true" 
    CodeBehind="GizmosCancelAsync.aspx.cs" 
    Inherits="WebAppAsync.GizmosCancelAsync" %>

В следующем коде показан файл GizmosCancelAsync.aspx.cs .

protected void Page_Load(object sender, EventArgs e)
{
    RegisterAsyncTask(new PageAsyncTask(GetGizmosSvcCancelAsync));
}

private async Task GetGizmosSvcCancelAsync(CancellationToken cancellationToken)
{
    var gizmoService = new GizmoService();
    var gizmoList = await gizmoService.GetGizmosAsync(cancellationToken);
    GizmosGridView.DataSource = gizmoList;
    GizmosGridView.DataBind();
}
private void Page_Error(object sender, EventArgs e)
{
    Exception exc = Server.GetLastError();

    if (exc is TimeoutException)
    {
        // Pass the error on to the Timeout Error page
        Server.Transfer("TimeoutErrorPage.aspx", true);
    }
}

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

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

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

  • Windows 7, Windows Vista, Windows 8 и все клиентские операционные системы 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 и перейдите в область Пулы приложений.
    • Щелкните правой кнопкой мыши целевой пул приложений и выберите Дополнительные параметры.
      Снимок экрана: диспетчер служб IIS с меню
    • В диалоговом окне Дополнительные параметры измените значение Параметра Длина очереди с 1000 на 5000.
      Снимок экрана: диалоговое окно

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

  • Управление версиями и многонацелие .NET — .NET 4.5 — это обновление на месте до .NET 4.0

  • Настройка приложения IIS или AppPool для использования ASP.NET 3.5, а не 2.0

  • Версии и зависимости платформы .NET Framework

  • Если приложение использует веб-службы или 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 должно быть в порядке.

Соавторы