Асинхронная обработка запросов
167ASP.NET --- ASP.NET Web Forms 4.5 --- Асинхронная обработка запросов
В этой и последующих статьях мы покажем, как обрабатывать запросы асинхронно. Это сложный прием, который требует понимания библиотеки параллельных задач .NET (Task Parallel Library - TPL) и параллельного программирования в целом. Мы объясним проблему, решаемую с помощью асинхронной обработки запросов, и продемонстрируем набор решений, однако не будем рассматривать основы параллельного программирования, равно как и его поддержку в .NET.
He применяйте приемы, показанные в этой и других статьях, если вы не знакомы с параллельным программированием - здесь очень легко получить неожиданное и непредсказуемое поведение приложения. Исходя из нашего опыта, ни одна тема не вызывает столько сложностей, как эта, и в большинстве проектов можно вполне обойтись использованием обычной синхронной обработки запросов.
Не путайте также асинхронную обработку запросов на сервере, которую мы будем здесь рассматривать, и асинхронное взаимодействие между сервером и клиентом (AJAX)! Это две совершенно разные темы.
Подготовка проекта для примера
В качестве примера мы создали новый проект под названием AsyncApp, используя шаблон ASP.NET Empty Web Application (Пустое веб-приложение ASP.NET) в Visual Studio. В проект добавлена веб-форма по имени Default.aspx с контентом, представленным в примере ниже:
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs"
Inherits="AsyncApp.Default" %>
<!DOCTYPE html>
<html>
<head runat="server">
<title></title>
<style>
table { border: thin solid black; border-collapse: collapse; font-family: Tahoma}
th, td { text-align: left; padding: 5px; border: thin solid black;}
</style>
</head>
<body>
<table>
<tr>
<th>URL</th>
<th>Длина, байт</th>
<th>Время запроса, мс</th>
<th>Общее время запроса, мс</th>
</tr>
<tr>
<td><%: GetResult().Url %></td>
<td><%: GetResult().Length %></td>
<td><%: GetResult().Blocked %></td>
<td><%: GetResult().Total%></td>
</tr>
</table>
</body>
</html>
Эта веб-форма содержит элемент <table> с одной строкой, которая заполняется с применением фрагментов кода, вызывающих метод GetResult(). Метод GetResult() и остаток класса отделенного кода можно видеть в примере ниже:
using System;
using System.Diagnostics;
using System.Net;
using System.Threading.Tasks;
using System.Web.UI;
namespace AsyncApp
{
public class WebSiteResult
{
public string Url { get; set; }
public long Length { get; set; }
public long Blocked { get; set; }
public long Total { get; set; }
}
public partial class Default : System.Web.UI.Page
{
private WebSiteResult result;
protected void Page_Load(object sender, EventArgs e)
{
string targetUrl = "http://professorweb.ru";
WebClient client = new WebClient();
result = new WebSiteResult { Url = targetUrl };
Stopwatch sw = Stopwatch.StartNew();
string webContent = client.DownloadString(targetUrl);
result.Length = webContent.Length;
result.Blocked = sw.ElapsedMilliseconds;
result.Total = (long)DateTime.Now.Subtract(Context.Timestamp).TotalMilliseconds;
}
public WebSiteResult GetResult()
{
return result;
}
}
}
Здесь создан класс WebSiteResult, в котором определены свойства, используемые внутри элемента <table> веб-формы. Эти свойства позволяют указать URL, объем данных, возвращенный после запроса, время, потраченное на получение данных и общее время обработки запроса. В методе Page_Load() создается объект WebSiteResult и присваивается полю result, которое затем применяется методом GetResult().
Внутри метода Page_Load() используются два класса, которые часто упоминаются при описании асинхронного программирования: System.Net.WebClient и System.Diagnostics.Stopwatch.
В классе WebClient определен метод по имени DownloadString(), который принимает URL в качестве аргумента - этот URL запрашивается, а контент, отправленный в ответ сервером, возвращается в виде значения string.
Класс Stopwatch - это таймер высокого разрешения, который удобен при измерении длительности выполнения операций. Статический метод StartNew() возвращает новый объект Stopwatch, который начинает измерение времени. Свойство ElapsedMilliseconds возвращает количество миллисекунд, прошедших с момента вызова метода StartNew().
В результате при поступлении события Load класс отделенного кода запрашивает URL вида "http://professorweb.ru" и генерирует объект WebSiteResult, который содержит сведения об объеме полученных данных, числе миллисекунд, потраченных на доставку данных, и количестве миллисекунд, ушедших на обработку запроса.
Мы несколько схитрили со свойством WebSiteResult.Total. Присваивая ему значение в обработчике события Load, мы слишком сокращаем продолжительность, поскольку среда ASP.NET Framework должна еще инициировать другие события жизненного цикла до того, как обработка запроса будет завершена. Тем не менее, для целей этих статей результат достаточно точен.
После этого значения из объекта WebSiteResult отображаются в браузере, как показано на рисунке ниже. После запуска приложения может быть получен другой результат - существует много вариаций, связанных с сетевыми запросами, к тому же контент нашего веб-сайта часто обновляется.
Суть проблемы
Веб-форма Default.aspx опирается на способ обработки запросов, чтобы представить проблему, которая может повлиять на общую производительность приложения.
Сервер приложений, на котором размещается среда ASP.NET Framework (обычно IIS, но может быть и настроенная служба внутри облачной платформы), поддерживает пул потоков, используемый для обработки входящих сетевых запросов (пул потоков подключений). По умолчанию поток назначается запросу в момент его поступления, продолжается в течение жизненного цикла обработки запросов ASP.NET и не освобождается до тех пор, пока не будет построен ответ. После отправки ответа поток возвращается в пул и становится доступным для обработки другого запроса, когда он поступит. Это называется синхронной обработкой запросов и именно с ней мы имели дело во всех примерах, рассмотренных до сих пор, включая веб-форму Default.aspx в примере приложения.
Потоки позволяют серверу получать и обрабатывать множество запросов одновременно. Грубо говоря, чем больше потоков имеется в пуле, тем больше запросов может быть обработано в заданный момент времени. Существует ограничение на количество потоков в пуле, которое обычно конфигурируется в соответствие с возможностями серверного оборудования - более мощное оборудование делает возможным наличие большего количества потоков в пуле и, следовательно, большего числа одновременно обрабатываемых запросов.
Когда все потоки из пула задействованы для обработки запросов, пул потоков исчерпывается, и больше нет потоков, готовых к обработке новых запросов в случае их поступления. Сервер будет помещать запросы в очередь на некоторое время в ожидании, что какой-то поток будет возвращен в пул и сможет использоваться для обработки запроса. После того, как очередь станет слишком длинной, сервер начнет отклонять новые запросы, возвращая HTTP-код 503, который сообщает браузеру о занятости сервера. Это обычно происходит в периоды пиковой нагрузки, когда пул потоков слишком мал, чтобы быть способным обрабатывать большое число входящих запросов.
В наших интересах удостовериться в том, что потоков в пуле достаточно для обслуживания всего объема запросов, получаемых приложением. Проще всего это сделать, нарастив вычислительные мощности - объем памяти, количество процессоров, серверов или способности облачных службы. Однако простейший путь является и самым дорогостоящим.
Можно также оптимизировать приложение, сократив время обработки каждого запроса и обеспечив более быстрый возврат потоков в пул. Мы применяем приемы, подобные кешированию вывода, чтобы можно было повторно использовать результаты, сгенерированные для предшествующих запросов, и пересматриваем код в поисках возможностей упорядочения способа, которым обрабатываются запросы.
Темой следующих статей является другой вид оптимизации - поиск ситуаций, при которых потоки выделены для запросов, но не делают какой-либо полезной работы; именно это присутствует внутри веб-формы Default.aspx. Точнее говоря, проблема возникает при вызове метода WebClient.DownloadString():
string webContent = client.DownloadString(targetUrl);
Поток для этого запроса вынужден ожидать ответа от удаленного веб-сервера, который должен отправить данные; на протяжении этого времени говорят, что поток заблокирован. Оптимизация заключается в освобождении этого потока, чтобы он мог обрабатывать другие запросы, пока не поступят данные от веб-сервера; такой подход называется асинхронной обработкой запросов.
Асинхронная обработка запросов не улучшает показатели производительности отдельных запросов. Как показано на рисунке выше, запрос потребовал около 1.8 секунды на получение данных и генерацию ответа - и он по-прежнему будет занимать примерно 1.8 секунды после применения асинхронной обработки. На самом деле запрос может даже выполняться дольше из-за накладных расходов, связанных с управлением асинхронными операциями.
Ключевым аспектом являются 0.8 секунды, в течение которых поток ожидает ответа от веб-сервера, что отражено в столбце "Время запроса" элемента <table>, сгенерированного веб-формой Default.aspx. За счет переключения на асинхронную обработку запросов можно освободить поток, когда он не нужен, чтобы он стал доступным для обработки других запросов - в результате это приводит к увеличению пропускной способности приложения.
Когда необходимо применять асинхронную обработку запросов?
Не все запросы требуют асинхронной обработки. Применяя асинхронную обработку запросов к веб-формам, которые не выигрывают от этого, в действительности можно даже снизить пропускную способность приложения. Описанные в следующих статьях приемы довольно сложны, возможно лучше заказать разработку веб-приложений. Однако, если вы сами хотите использовать эти приемы, то должны знать в каких случаях необходимо их применять:
Действие, которое необходимо выполнить, доступно через асинхронный метод (метод, возвращающий объект Task и аннотированный ключевым словом async).
Действие интенсивно использует ввод-вывод (т.е. ожидает дискового или сетевого ввода), а не центральный процессор (т.е. требует большого объема обращений к процессору для выполнения вычислений).
Тестирование приложения показало, что простаивающие потоки ограничивают общую пропускную способность приложения.
Улучшения общей пропускной способности оправдывают затраты на написание, тестирование и сопровождение сложного кода.
Как видите, асинхронная обработка запросов является узкоспециализированным приемом. Она решает проблемы весьма специфического типа и должна применяться только при наличии четкого понимания, каковым будет результирующее влияние. Асинхронное программирование представляет собой сложную технологию, и вы столкнетесь со многими трудностями, если не владеете основами параллельного выполнения и управления потоками.