Асинхронные веб-формы
121ASP.NET --- ASP.NET Web Forms 4.5 --- Асинхронные веб-формы
Чтобы обрабатывать запрос асинхронным образом, необходимо сообщить среде ASP.NET Framework о том, что мы намерены делать, а также указать, когда поток не должен ожидать. Первая часть, объявление своего намерения среде ASP.NET, реализована в файле Default.aspx, как показано в примере ниже:
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs"
Inherits="AsyncApp.Default" Async="true" AsyncTimeout="60" %>
...
Установка атрибута Async директивы Page в true сообщает среде ASP.NET, что мы хотим использовать асинхронную обработку запросов внутри веб-формы. Эта функция должна включаться явно, т.к. установка и поддержка ресурсов, требуемых для асинхронных задач, сопряжены с определенными накладными расходами, которые не являются желательными, когда асинхронная обработка не нужна.
В атрибуте AsyncTimeout указано количество секунд, в течение которых среда ASP.NET будет ожидать завершения асинхронных задач перед тем, как возникнет тайм-аут запроса. В примере он был установлен в 60 секунд; если атрибут AsyncTimeout не указан, для него принимается стандартное значение, равное 45 секунд. Одна из причин применения асинхронной обработки запросов связана с необходимостью согласования с длительными операциями, поэтому вы должны настроить значение AsyncTimeout так, чтобы позволить выполняемой работе благополучно завершиться.
Асинхронная обработка реализована в файле отделенного кода Default.aspx.cs, содержимое которого приведено в примере ниже:
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();
// Асинхронное выполнение запроса
RegisterAsyncTask(new PageAsyncTask(async () =>
{
string webContent = await client.DownloadStringTaskAsync(targetUrl);
result.Length = webContent.Length;
result.Total
= (long)DateTime.Now.Subtract(Context.Timestamp).TotalMilliseconds;
}));
result.Blocked = sw.ElapsedMilliseconds;
}
public WebSiteResult GetResult()
{
return result;
}
}
}
Изменения выглядят мелкими, но в них выполняется большой объем работ. Вскоре мы объясним все детали, а пока рассмотрим, какой получен результат. На рисунке ниже показано, что произошло после запуска приложения и запроса веб-формы Default.aspx:
Общее время, потраченное на обработку запроса, получение данных от удаленного веб-сервера и генерацию ответа слегка увеличилось по сравнения с синхронным вариантом, рассмотренным в предыдущей статье (это связано в основном с варьированием возможностей и маршрутизации в Интернете). Крупное отличие заключается в том, что поток запроса больше не блокируется во время ожидания HTTP-запроса к http://professorweb.ru для получения ответа.
За счет применения асинхронной обработки к этой веб-форме появилась возможность вернуть поток запроса в пул на период в 1.5 секунды, который был затрачен на получение ответа от сервера. В течение этого времени данный поток был доступен для обработки других запросов, что привело к улучшению общей пропускной способности приложения.
Мы получили результат, который указывает на отсутствие времени блокирования, но на самом деле это немного не так; некоторое блокирование присутствует всегда, даже если оно существует на протяжении очень короткого времени, поскольку требуется время на установку инфраструктуры, которая позаботится о выполнении необходимой фоновой работы. Пример запускался на локальном сервере, а измерения проводились с точностью до миллисекунды - с помощью более точного таймера и при реалистичной рабочей нагрузке накладные расходы будут несколько выше.
Использование асинхронного метода
Первое изменение, которое было внесено в файл Default.aspx.cs, касалось использования другого метода класса WebClient:
string webContent = await client.DownloadStringTaskAsync(targetUrl);
Вместо синхронного метода DownloadString() вызывается асинхронный вариант DownloadStringTaskAsync(), который возвращает объект Task<string> и может применяться с ключевым словом await. Метод DownloadStringTaskAsync() дает тот же самый результат, что и метод DownLoadString(), но работает асинхронным образом, не блокируя поток запроса.
Создание собственных асинхронных методов
Можно переписать любой метод, обеспечив возврат из него объекта Task и использование ключевого слова async, но поскольку поток по-прежнему должен выполнять этот метод, возникает риск переноса той же самой проблемы в другое место. В широком смысле существуют два исключения из этого:
Первое исключение предусматривает передачу запроса другому, более ограниченному пулу потоков, такому как пул, применяемый в библиотеке параллельных задач (TPL). Пул потоков TPL использует небольшое количество потоков и обладает средствами сокращения времени, которое потоки тратят на блокирование.
Опасность здесь в том, что можно освобождать поток во избежание исчерпания пула потоков подключений, но в конечном итоге израсходовать пул меньшего размера. Например, библиотека TPL выделяет по одному потоку на каждый процессор, и даже при наличии в ней расширенных средств легко получить очередь работ такой длины, что HTTP-запросы начнут прекращаться по тайм-ауту.
Второе исключение связано с применением низкоуровневых оптимизаций, которые увеличивают эффективность, не связывая потоки. Хорошим примером могут служить порты завершения ввода-вывода, которые операционная система Windows реализует для того, чтобы позволить управлять большими количествами потоков ввода-вывода с помощью небольшого числа потоков. Это средство используется классом WebClient, и оно также применяется некоторыми поставщиками подключений к базам данных.
Запомните в качестве эмпирического правила: если вы не имеете существенного опыта параллельного программирования, то должны применять асинхронную обработку запросов к веб-формам ASP.NET только с использованием классов .NET Framework, предоставляющих асинхронные методы, таких как WebClient.
Создание и регистрация асинхронной страничной задачи
Нам необходимо упаковать асинхронную работу, предназначенную для выполнения, чтобы ее можно было интегрировать в жизненный цикл, который определен классом Page. Это делается с применением класса PageAsyncTask, конструктор которого принимает делегат, возвращающий объект Task. Мы используем лямбда-выражение следующего вида:
// ...
new PageAsyncTask(async () =>
{
string webContent = await client.DownloadStringTaskAsync(targetUrl);
result.Length = webContent.Length;
result.Total
= (long)DateTime.Now.Subtract(Context.Timestamp).TotalMilliseconds;
})
// ...
Это расширенное применение синтаксиса лямбда-выражений, которое упрощает определение фоновой работы, не требуя создание отдельного асинхронного метода. Компилятор C# выполняет ряд "магических" действий с ключевыми словами async (которое применяется к лямбда-выражению) и await (которое используется в качестве префикса при вызове метода класса WebClient). Результатом является делегат, который возвращает объект Task и вызывает метод DownloadStringTaskAsync(), не блокируя поток запроса.
Асинхронное действие регистрируется за счет передачи объекта PageAsyncTask методу RegisterAsyncTask(), определенному в классе Page:
RegisterAsyncTask(new PageAsyncTask(async () =>
{
// ...
}));
Метод RegisterAsyncTask() регистрирует асинхронный объект, но его выполнение будет инициировано только после события PreRender и перед событием PreRenderComplete.
Выполнение множества задач
Можно зарегистрировать несколько асинхронных задач и все они будут выполнены после возникновения события PreRenderComplete - однако последовательно, а не параллельно. Это не блокирует поток запроса, но не дает эффект, который большинство людей ожидают получить, когда имеют дело с множеством асинхронных операций.
Для демонстрации того, что произойдет, мы добавили новую веб-форму по имени Multiples.aspx, контент которой показан в примере ниже:
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Multiples.aspx.cs"
Inherits="AsyncApp.Multiples" Async="true" %>
<!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>Время начала</th><th>URL</th><th>Длина</th></tr>
<asp:Repeater id="rep" SelectMethod="GetResults"
ItemType="AsyncApp.MultiWebSiteResult" runat="server">
<ItemTemplate>
<tr>
<td><%# Item.StartTime %></td>
<td><%# Item.Url %></td>
<td><%# Item.Length %></td>
</tr>
</ItemTemplate>
</asp:Repeater>
</table>
</body>
</html>
Это вариация веб-формы Default.aspx, созданной ранее. С помощью элемента управления Repeater отображается набор результатов. Кроме того, определен другой тип со свойствами, имеющими отношение к этому примеру - в частности, нас интересует время начала запроса к веб-серверу, а не промежуток времени, в течение которого поток запроса был заблокирован, или время выполнения отдельных запросов. Определения типа и класса отделенного кода представлены в примере ниже:
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Net;
using System.Web.UI;
using System.Threading.Tasks;
namespace AsyncApp
{
public class MultiWebSiteResult
{
public string Url { get; set; }
public long Length { get; set; }
public long StartTime { get; set; }
}
public partial class Multiples : System.Web.UI.Page
{
private ConcurrentQueue<MultiWebSiteResult> results;
protected void Page_Load(object sender, EventArgs e)
{
string[] targetUrls
= { "http://asp.net", "http://professorweb.ru", "http://google.com" };
results = new ConcurrentQueue<MultiWebSiteResult>();
foreach (string targetUrl in targetUrls)
{
MultiWebSiteResult result = new MultiWebSiteResult { Url = targetUrl };
results.Enqueue(result);
RegisterAsyncTask(new PageAsyncTask(async () =>
{
result.StartTime
= (long)DateTime.Now.Subtract(Context.Timestamp).TotalMilliseconds;
string webContent = await new WebClient().DownloadStringTaskAsync(targetUrl);
result.Length = webContent.Length;
rep.DataBind();
}));
}
}
public IEnumerable<MultiWebSiteResult> GetResults()
{
return results;
}
}
}
Мы запрашиваем три URL и назначаем запросы объектам PageAsyncTask. Для просмотра результатов запустите приложение и перейдите на URL вида /Multiples.aspx:
Вызов метода DataBind() гарантирует отображение результатов внутри элемента управления Repeater. Причина связана со способом интеграции элементов управления, отображающих данные, в жизненный цикл запроса Page.
По информации в столбце "Время начала" видно, что запросы выполнялись последовательно, т.е. запрос к google.com не запускался, пока не прошло около 2.3 секунд после его получения средой ASP.NET.
Это может оказаться приемлемым в вашем приложении, но в большинстве приложений требуется помещать работу в очередь, чтобы отдельные запросы можно было выполнять параллельно при наличии достаточного числа доступных потоков. Достичь такого результата проще всего с помощью библиотеки TPL - в примере ниже показано, как это сделано в файле отделенного кода Multiples.aspx.cs:
// ...
protected void Page_Load(object sender, EventArgs e)
{
string[] targetUrls
= { "http://asp.net", "http://professorweb.ru", "http://google.com" };
results = new ConcurrentQueue<MultiWebSiteResult>();
RegisterAsyncTask(new PageAsyncTask(async () =>
{
List<Task> tasks = new List<Task>();
foreach (string targetUrl in targetUrls)
{
tasks.Add(Task.Factory.StartNew(() =>
{
MultiWebSiteResult result = new MultiWebSiteResult { Url = targetUrl };
result.StartTime
= (long)DateTime.Now.Subtract(Context.Timestamp).TotalMilliseconds;
Task<string> innerTask
= new WebClient().DownloadStringTaskAsync(targetUrl);
innerTask.Wait();
result.Length = innerTask.Result.Length;
results.Enqueue(result);
}));
}
await Task.WhenAll(tasks);
rep.DataBind();
}));
}
// ...
Мы регистрируем только один объект PageAsyncTask и управляем параллельными задачами самостоятельно с применением класса Task. Как упоминалось в предыдущей статье, библиотека TPL является отдельной крупной темой, и мы не собираемся здесь подробно объяснять этот код. Отметим только, что в результате обеспечивается помещение в очередь отдельных запросов к контенту веб-сайта, что позволит выполнить их параллельно:
Как видите, все три запроса были запущены почти в одно и то же время. Это не гарантированно произойдет, т.к. в очереди могут находиться другие работы, а все потоки TPL могут быть заняты, но если есть свободные системные ресурсы, то время, затраченное на обработку запросов, будет намного меньше.