Wicked Code
Aplicativos escalonáveis com programação assíncrona em ASP.NET
Jeff Prosise
Download do código disponível em:
WickedCode2007_03.exe
(202 KB)
Browse the Code Online

Conteúdo
Quer saber um segredo? Um segredo sujo e obsceno? Um segredo que, se revelado, causaria grande comoção na comunidade ASP.NET e gritos instantâneos de “Arrá!” da multidão anti-Microsoft?
A maioria dos sites da Web criados com o ASP.NET não são muito escalonáveis. Eles possuem uma barreira invisível que limita o número de solicitações que podem ser processadas por segundo. Esses sites são perfeitamente escalonáveis até que o tráfego atinja o nível dessa barreira invisível. Depois, a taxa de transferência começa a cair. Logo depois, as solicitações começam a falhar, geralmente retornando erros de “Servidor indisponível”.
A razão subjacente já foi discutida muitas vezes na MSDN®Magazine. O ASP.NET usa threads de um pool de threads do CLR (Common Language Runtime) para processar solicitações. Contanto que haja threads disponíveis no pool de threads, o ASP.NET não enfrenta problemas para despachar solicitações de entrada. Mas quando o pool de threads fica saturado, ou seja, todos os seus threads estão ocupados processando solicitações e não há threads livres, as novas solicitações têm que aguardar que os threads sejam liberados. Se a interrupção se tornar muito severa e a fila atingir o limite de capacidade, o ASP.NET levanta as mãos e diz “Agora não!” para as novas solicitações.
Uma solução é aumentar o tamanho máximo do pool de threads, permitindo que mais threads sejam criados. Esse é o procedimento que os desenvolvedores geralmente realizam quando seus clientes reportam erros repetidos de “Servidor indisponível”. Outra estratégia comum é apelar para o hardware a fim de resolver o problema, adicionando mais servidores ao farm da Web.
Mas aumentar o número de threads ou de servidores não soluciona o problema. Isso só dá um alívio temporário para o que é, na realidade, um problema de design, não do próprio ASP.NET, mas da implementação do site em si. O problema real dos aplicativos que não são escalonáveis não é a falta de threads. Mas sim, o uso ineficiente dos threads que já existem.
Um site da Web em ASP.NET realmente escalonável utiliza de forma ideal o pool de threads. Ou seja, certificando-se de que os threads que processam as solicitações estão executando código em vez de esperando que a E/S seja concluída. Se o pool de threads ficar saturado devido a todos os threads se debatendo na CPU, não há muito a fazer além de adicionar servidores.
Entretanto, a maioria dos aplicativos da Web se comunica com bancos de dados, serviços da Web ou outras entidades externas, e limitam a escalabilidade forçando os threads do pool de threads a aguardar que consultas a bancos de dados, chamadas de serviço da Web e outras operações de E/S sejam concluídas. Uma solicitação direcionada a uma página da Web orientada a dados pode gastar uma fração de segundo executando código e vários segundos aguardando que uma consulta a banco de dados seja retornada. Enquanto a consulta estiver pendente, o thread atribuído à solicitação não poderá atender a outras solicitações. Essa é a barreira invisível. E é esse tipo de situação que você deve evitar caso deseje criar sites da Web altamente escalonáveis. Lembre-se: quando se trata de produtividade, a E/S não é boa idéia a menos que seja tratada de forma adequada.
Assim, a E/S poderá ser uma boa idéia se não prejudicar o pool de threads. E o ASP.NET oferece suporte a três modelos de programação assíncrona que funcionam como agentes protetores. A comunidade praticamente desconhece esses modelos, em parte devido à documentação escassa. Contudo, conhecer e saber como usá-los é absolutamente essencial para a criação de sites da Web avançados.
Páginas assíncronas
O primeiro, e geralmente o mais útil, dos três modelos de programação assíncrona ao qual o ASP.NET oferece suporte é a página assíncrona. Dos três modelos, esse é o único específico do ASP.NET 2.0. Os outros possuem suporte na versão 1.0.
Eu não vou tratar das páginas assíncronas detalhadamente aqui porque fiz isso na edição de outubro de 2005 (
msdn.microsoft.com/msdnmag/issues/05/10/WickedCode) (em inglês). O resultado é que se você tiver páginas que realizem operações de E/S relativamente longas, elas serão candidatas a se tornarem páginas assíncronas. Se uma página consultar um banco de dados e a consulta demorar, digamos, 5 segundos para retornar seja porque ela está retornando uma grande quantidade de dados ou está direcionada a um banco de dados remoto em uma conexão muito carregada, serão 5 segundos durante os quais o thread atribuído à solicitação não poderá ser usado para outras solicitações. Se toda solicitação tivesse esse comportamento, o aplicativo travaria rapidamente.
A Figura 1 ilustra como uma página assíncrona soluciona esse problema de forma organizada. Quando a solicitação chega, um thread é atribuído a ela pelo ASP.NET. A solicitação começa a ser processada nesse thread, mas quando chega o momento de atingir o banco de dados, a solicitação inicia uma consulta ADO.NET assíncrona e retorna o thread ao pool de threads. Quando a consulta é concluída, o ADO.NET chama novamente o ASP.NET e ele captura um outro thread no pool de threads e reinicia o processamento da solicitação.
Figura 1 Páginas assíncronas em uso (Clique na imagem para aumentar a exibição)
Enquanto a consulta estiver pendente, nenhum thread do pool de threads é consumido, deixando todos os threads livres para atender às solicitações de entrada. Uma solicitação processada de forma assíncrona não é executada mais rapidamente. Mas outras solicitações são executadas mais rapidamente porque não precisam aguardar que os threads sejam liberados. As solicitações incorrem em menos atraso ao entrar no pipeline e a taxa de transferência geral sobe.
A Figura 2 mostra a classe por trás do código de uma página assíncrona que realiza a ligação de dados em um banco de dados do SQL Server™. O método Page_Load chama AddOnPreRenderCompleteAsync para registrar os manipuladores inicial e final. Mais tarde, enquanto dura a solicitação, o ASP.NET chama o método begin, que inicia uma consulta ADO.NET assíncrona e retorna imediatamente, depois disso, o thread atribuído à solicitação retorna para o pool de threads. Quando o ADO.NET sinaliza que a consulta foi concluída, o ASP.NET recupera um thread do pool de threads (não necessariamente o mesmo usado antes) e chama o método end. O método End captura os resultados da consulta e o restante da solicitação é executado normalmente no thread que executou o método End.

Figure 2 Página assíncrona
using System;
using System.Data;
using System.Data.SqlClient;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.Configuration;
public partial class AsyncDataBind : System.Web.UI.Page
{
private SqlConnection _connection;
private SqlCommand _command;
private SqlDataReader _reader;
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
AddOnPreRenderCompleteAsync(
new BeginEventHandler(BeginAsyncOperation),
new EndEventHandler(EndAsyncOperation)
);
}
}
IAsyncResult BeginAsyncOperation (object sender, EventArgs e,
AsyncCallback cb, object state)
{
string connect = WebConfigurationManager.ConnectionStrings
["AsyncPubs"].ConnectionString;
_connection = new SqlConnection(connect);
_connection.Open();
_command = new SqlCommand(
"SELECT title_id, title, price FROM titles", _connection);
return _command.BeginExecuteReader (cb, state);
}
void EndAsyncOperation(IAsyncResult ar)
{
_reader = _command.EndExecuteReader(ar);
}
protected void Page_PreRenderComplete(object sender, EventArgs e)
{
Output.DataSource = _reader;
Output.DataBind();
}
public override void Dispose()
{
if (_connection != null) _connection.Close();
base.Dispose();
}
}
O atributo Async="true" na diretiva Page de ASPX não é mostrado na Figura 2. Ele é necessário para uma página assíncrona: ele sinaliza para o ASP.NET implementar a interface IHttpAsyncHandler na página (mais detalhes sobre isso em breve). Também não é mostrada na Figura 2 a seqüência de caracteres de conexão do banco de dados, que inclui um atributo Async="true" próprio para que o ADO.NET saiba realizar uma consulta assíncrona.
AddOnPreRenderCompleteAsync é uma maneira de estruturar uma página assíncrona. Outra maneira é chamar RegisterAsyncTask. Ele possui algumas vantagens sobre AddOnPreRenderCompleteAsync: a mais importante delas é que ele simplifica a tarefa de realização de várias operações de E/S assíncronas em uma solicitação. Para obter detalhes sobre ele, consulte a edição de outubro de 2005 de Wicked Code.
Manipuladores HTTP assíncronos
O segundo modelo de programação assíncrona realizado no ASP.NET é o manipulador HTTP assíncrono. Um manipulador HTTP é um objeto que serve como ponto de extremidade de solicitações. As solicitações por arquivos ASPX, por exemplo, são processadas por um manipulador HTTP para arquivos ASPX. Da mesma forma, as solicitações por arquivos ASMX são manipuladas por um manipulador HTTP que sabe como lidar com serviços ASMX. Na verdade, o ASP.NET vem com manipuladores HTTP para vários tipos de arquivos. Você pode ver esses tipos de arquivos, e os manipuladores HTTP correspondentes, na seção <httpHandlers> do arquivo web.config mestre (no ASP.NET 1.x, ela está em machine.config).
Você pode estender o ASP.NET para oferecer suporte a tipos de arquivos adicionais escrevendo manipuladores HTTP personalizados. Mas ainda mais interessante é o fato de que você pode implantar manipuladores HTTP personalizados em arquivos ASHX e usá-los como destinos de solicitações HTTP. Essa é a maneira correta de criar pontos de extremidade da Web que gerem imagens rapidamente ou recuperem imagens de bancos de dados. Basta incluir uma marca <img> (ou controle Image) na página e apontá-la para um ASHX que crie ou busque a imagem. Direcionar um arquivo ASHX com solicitações é mais eficiente que direcionar um arquivo ASPX porque um arquivo ASHX incorre em menos sobrecarga no tempo de processamento.
Por definição, os manipuladores HTTP implementam a interface IHttpHandler. Os manipuladores que implementam essa interface realizam seu processamento de forma síncrona. O arquivo ASHX na Figura 3 contém um manipulador http desse tipo. No tempo de execução, o TerraServiceImageGrabber faz várias chamadas para o serviço da Web Microsoft® TerraServer para converter uma cidade e um estado em latitude e longitude, recuperar imagens de satélite ("quadros") e juntar imagens para formar uma imagem composta do local especificado.

Figure 3 Manipulador HTTP síncrono
<%@ WebHandler Language="C#" Class="TerraServiceImageGrabber" %>
using System;
using System.Web;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
public class TerraServiceImageGrabber : IHttpHandler
{
public void ProcessRequest (HttpContext context)
{
// Extract user input from the query string
string city = context.Request["City"];
string state = context.Request["State"];
string scale = context.Request["Scale"];
// If city or state wasn't specified, throw an exception
if (String.IsNullOrEmpty(city) || String.IsNullOrEmpty(state))
throw new ArgumentException(
"City and state must be specified via query string");
// Determine the scale
Scale res = Scale.Scale8m;
if (!String.IsNullOrEmpty (scale))
{
switch (scale)
{
case "1":
res = Scale.Scale1m;
break;
case "2":
res = Scale.Scale2m;
break;
case "4":
res = Scale.Scale4m;
break;
case "8":
res = Scale.Scale8m;
break;
case "16":
res = Scale.Scale16m;
break;
case "32":
res = Scale.Scale32m;
break;
}
}
// Generate the requested image
using(Bitmap bitmap = GetTiledImage(city, state, res, 900, 600))
{
// Set the response's content type
context.Response.ContentType = "image/jpeg";
// Write the image to the HTTP response
bitmap.Save(context.Response.OutputStream, ImageFormat.Jpeg);
}
}
public bool IsReusable { get { return true; } }
private Bitmap GetTiledImage (string city, string state,
Scale scale, int cx, int cy)
{
// Instantiate the TerraService proxy
TerraService ts = new TerraService ();
// Get the latitude and longitude of the requested city
Place place = new Place ();
place.City = city;
place.State = state;
place.Country = "USA";
LonLatPt point = ts.ConvertPlaceToLonLatPt (place);
// Compute the parameters for a bounding box
AreaBoundingBox abb = ts.GetAreaFromPt (point, Theme.Photo,
scale, cx, cy);
// Create an image to fit the bounding box
Bitmap bitmap = new Bitmap (cx, cy, PixelFormat.Format32bppRgb);
using(Graphics g = Graphics.FromImage(bitmap))
{
int x1 = abb.NorthWest.TileMeta.Id.X;
int y1 = abb.NorthWest.TileMeta.Id.Y;
int x2 = abb.NorthEast.TileMeta.Id.X;
int y2 = abb.SouthWest.TileMeta.Id.Y;
for (int x=x1; x<=x2; x++) {
for (int y=y1; y>=y2; y--) {
TileId tid = abb.NorthWest.TileMeta.Id;
tid.X = x;
tid.Y = y;
using(Image tile = Image.FromStream(
new MemoryStream(ts.GetTile(tid))))
{
g.DrawImage(tile,
(x - x1) * tile.Width -
(int) abb.NorthWest.Offset.XOffset,
(y1 - y) * tile.Height -
(int) abb.NorthWest.Offset.YOffset,
tile.Width, tile.Height);
}
}
}
}
// Return the image
return bitmap;
}
}
Os resultados são impressionantes. Mas há um problema. O TerraServiceImageGrabber é um exemplo perfeito de como não se deve escrever manipuladores HTTP. Pense nisso. O TerraServiceImageGrabber requer vários segundos (pelo menos) para fazer todas as chamadas de serviço da Web e processar os resultados. Grande parte do tempo é gasto simplesmente aguardando-se que as chamadas de serviço da Web sejam concluídas. Solicitações repetidas pelo arquivo ASHX podem esgotar o pool de threads no ASP.NET em uma fração de segundo, impedindo que outras páginas no aplicativo sejam apresentadas (ou pelo menos forçando o seu enfileiramento até que um thread torne-se disponível). Você não pode criar um aplicativo escalonável dessa forma a menos que dimensione o hardware. Mas por que gastar muito dinheiro em um farm da Web se um servidor pode lidar com a carga usando um software escrito adequadamente?
Figura 4 TerraServiceImageGrabber em uso (Clique na imagem para aumentar a exibição)
Os manipuladores HTTP não precisam ser síncronos. Ao implementar a interface IhttpAsyncHandler, que é derivada de IHttpHandler, um manipulador HTTP pode ser assíncrono. Quando usado corretamente, um manipulador assíncrono utiliza threads do ASP.NET de forma mais eficiente. Isso é feito da mesma maneira que uma página assíncrona. Na verdade, as páginas assíncronas utilizam o suporte ao manipulador assíncrono que antecedeu as páginas assíncronas no ASP.NET.
A Figura 5 contém uma versão assíncrona do manipulador mostrado na Figura 3. O Async-TerraServiceImageGrabber é um pouco mais complexo, mas muito mais escalonável.

Figure 5 Manipulador HTTP assíncrono
<%@ WebHandler Language="C#" Class="AsyncTerraServiceImageGrabber" %>
using System;
using System.Web;
using System.Drawing;
using System.Drawing.Imaging;
using System.Threading;
using System.IO;
public class AsyncTerraServiceImageGrabber : IHttpAsyncHandler
{
private TerraService _ts;
private TerraServiceAsyncResult _ar;
private Scale _scale = Scale.Scale8m;
private AreaBoundingBox _abb;
private Bitmap _bitmap;
private int _count = 0;
private int _max;
private HttpContext _context;
private Exception _ex;
private int _cx = 900, _cy = 600; // Width and height of bitmap
public void ProcessRequest (HttpContext context)
{
// Never called
}
public bool IsReusable { get { return false; } }
public IAsyncResult BeginProcessRequest(HttpContext context,
AsyncCallback cb, object state)
{
_context = context;
// Extract user input from the query string
string city = context.Request["City"];
string region = context.Request["State"];
string scale = context.Request["Scale"];
// If city or state wasn’t specified, throw an exception
if (String.IsNullOrEmpty(city) || String.IsNullOrEmpty(region))
throw new ArgumentException(
"City and state must be specified via query string");
// Determine the scale
if (!String.IsNullOrEmpty (scale))
{
switch (scale)
{
case "1":
_scale = Scale.Scale1m;
break;
case "2":
_scale = Scale.Scale2m;
break;
case "4":
_scale = Scale.Scale4m;
break;
case "8":
_scale = Scale.Scale8m;
break;
case "16":
_scale = Scale.Scale16m;
break;
case "32":
_scale = Scale.Scale32m;
break;
}
}
// Instantiate a TerraServiceAsyncResult
_ar = new TerraServiceAsyncResult(cb, state);
// Instantiate the TerraService proxy
_ts = new TerraService();
// Make an async call to get the latitude and longitude
// of the requested city
Place place = new Place();
place.City = city;
place.State = region;
place.Country = "USA";
_ts.BeginConvertPlaceToLonLatPt(place,
new AsyncCallback(ConvertPlaceToLonLatCompleted), null);
// Return an IAsyncResult that delays EndProcessRequest until
// the final asynchronous Web service call has completed
return _ar;
}
private void ConvertPlaceToLonLatCompleted(IAsyncResult ar)
{
try
{
// Complete the async call
LonLatPt point = _ts.EndConvertPlaceToLonLatPt(ar);
// Make an async call to compute the parameters
// for a bounding box
_ts.BeginGetAreaFromPt(point, Theme.Photo, _scale, _cx,
_cy, new AsyncCallback(GetAreaFromPointCompleted), null);
}
catch (Exception ex)
{
_ex = ex;
_ar.CompleteCall();
}
}
private void GetAreaFromPointCompleted(IAsyncResult ar)
{
try
{
// Complete the async call
_abb = _ts.EndGetAreaFromPt(ar);
// Create an image to fit the bounding box
_bitmap = new Bitmap(_cx, _cy, PixelFormat.Format32bppRgb);
int x1 = _abb.NorthWest.TileMeta.Id.X;
int y1 = _abb.NorthWest.TileMeta.Id.Y;
int x2 = _abb.NorthEast.TileMeta.Id.X;
int y2 = _abb.SouthWest.TileMeta.Id.Y;
_max = (x2 - x1 + 1) * (y1 - y2 + 1);
// Place concurrent async calls to TerraService to
// fetch image tiles
for (int x = x1; x <= x2; x++)
{
for (int y = y1; y >= y2; y--)
{
TileId tid = new TileId();
tid.Theme = _abb.NorthWest.TileMeta.Id.Theme;
tid.Scale = _abb.NorthWest.TileMeta.Id.Scale;
tid.Scene = _abb.NorthWest.TileMeta.Id.Scene;
tid.X = x;
tid.Y = y;
_ts.BeginGetTile(tid,
new AsyncCallback(GetTileCompleted),
new Point(x - x1, y1 - y));
}
}
}
catch (Exception ex)
{
_ex = ex;
_ar.CompleteCall();
}
}
private void GetTileCompleted(IAsyncResult ar)
{
try
{
// Complete the async call
using(Image tile = Image.FromStream(
new MemoryStream(_ts.EndGetTile(ar))))
{
// Draw the tile onto the bitmap
Point point = (Point)ar.AsyncState;
int dx = point.X;
int dy = point.Y;
lock (_bitmap)
{
using(Graphics g = Graphics.FromImage(_bitmap))
{
g.DrawImage(tile,
dx * tile.Width –
(int)_abb.NorthWest.Offset.XOffset,
dy * tile.Height –
(int)_abb.NorthWest.Offset.YOffset,
tile.Width, tile.Height);
}
}
// Increment the tile count and complete the request if all
// tiles have been fetched
int count = Interlocked.Increment(ref _count);
if (count == _max) _ar.CompleteCall();
}
catch (Exception ex)
{
_ex = ex;
_ar.CompleteCall();
}
}
public void EndProcessRequest(IAsyncResult ar)
{
if (_ex != null)
{
// If an exception was thrown, rethrow it
throw _ex;
}
else
{
// Otherwise return the generated image
_context.Response.ContentType = "image/jpeg";
_bitmap.Save(_context.Response.OutputStream,
ImageFormat.Jpeg);
_bitmap.Dispose();
}
}
}
class TerraServiceAsyncResult : IAsyncResult
{
private AsyncCallback _cb;
private object _state;
private ManualResetEvent _event;
private bool _completed = false;
private object _lock = new object();
public TerraServiceAsyncResult(AsyncCallback cb, object state)
{
_cb = cb;
_state = state;
}
public Object AsyncState { get { return _state; } }
public bool CompletedSynchronously { get { return false; } }
public bool IsCompleted { get { return _completed; } }
public WaitHandle AsyncWaitHandle
{
get
{
lock (_lock)
{
if (_event == null)
_event = new ManualResetEvent(IsCompleted);
return _event;
}
}
}
public void CompleteCall()
{
lock (_lock)
{
_completed = true;
if (_event != null) _event.Set();
}
if (_cb != null) _cb(this);
}
}
O processamento assíncrono começa quando o ASP.NET chama o método BeginProcessRequest do manipulador. O BeginProcessRequest faz uma chamada assíncrona para o TerraService através do método BeginConvertPlaceToLonLatPt do proxy do TerraService. O thread atribuído à solicitação retorna então ao pool de threads. Quando a chamada assíncrona é concluída, outro thread é obtido do pool de threads para executar o método ConvertPlaceToLonLatCompleted. Esse thread recupera os resultados da última chamada, faz uma chamada assíncrona própria e depois volta para o pool de threads. Esse padrão se repete até que todas as chamadas assíncronas sejam concluídas. Nesse momento, o método EndProcessRequest do manipulador é chamado e o Bitmap resultante é retornado para o solicitante.
Para impedir que o EndProcessRequest ocorra antes que a chamada final do serviço da Web tenha sido concluída, o AsyncTerraServiceImageGrabber retorna sua própria implementação de IAsyncResult a partir de BeginProcessRequest. Se ele retornasse o IAsyncResult retornado por BeginConvertPlaceToLonLatPt, EndProcessRequest seria chamado (e a solicitação seria encerrada) no momento em que a primeira chamada do serviço da Web fosse concluída.
A classe que implementa IAsyncResult, TerraServiceAsyncResult, apresenta um método público CompleteCall que pode ser chamado a qualquer momento para finalizar a solicitação. Normalmente, AsyncTerraServiceImageGrabber chama CompleteCall somente depois que a chamada final do serviço da Web tiver sido concluída. Entretanto, se uma exceção for gerada em um dos métodos que são executados entre BeginProcessRequest e EndProcessRequest, o manipulador armazena a exceção em um campo particular (_ex), chama CompleteCall para encerrar a solicitação e depois gera novamente a exceção de EndProcessRequest. Do contrário, a exceção se perderia e a solicitação nunca seria concluída.
AsyncTerraServiceImageGrabber é mais escalonável que seu equivalente síncrono pois consome threads do ASP.NET durante somente uma fração do tempo total necessário para processar uma solicitação. Grande parte do tempo é gasto simplesmente aguardando-se que a chamada assíncrona do serviço da Web seja concluída.
Em teoria, AsyncTerraServiceImageGrabber também pode realizar o TerraServiceImageGrabber pois, em vez de fazer chamadas repetidas em série para o método GetTile de TerraService, ele faz as chamadas em paralelo. Na realidade, entretanto, somente duas chamadas de saída direcionadas a um determinado endereço IP podem ficar pendentes por vez, a menos que você aumente a configuração padrão de maxconnection do tempo de execução:
<system.net>
<connectionManagement>
<add address="*" maxconnection="20" />
</connectionManagement>
</system.net>
Outras definições de configuração também podem afetar a concorrência. Para obter mais informações, consulte o artigo da Base de Dados de Conhecimento "Contenção, mau desempenho e deadlocks ao fazer solicitações de serviço Web pelos aplicativos ASP.NET" (
support.microsoft.com/kb/821268).
Mesmo que somente uma chamada seja executada por vez, o AsyncTerraServiceImageGrabber não deve ter um desempenho pior que o TerraServiceImageGrabber. E o seu design é muito superior porque ele usa threads do ASP.NET da forma mais eficiente possível.
Módulos HTTP assíncronos
O terceiro modelo de programação assíncrona que você pode usar no ASP.NET é o módulo HTTP assíncrono. Um módulo HTTP é um objeto situado no pipeline do ASP.NET, onde ele pode ver, e até mesmo modificar, solicitações de entrada e respostas de saída. Muitos dos principais serviços no ASP.NET são implementados na forma de módulos HTTP, incluindo autenticação, autorização e cache de saída. Você pode estender o ASP.NET escrevendo módulos HTTP personalizados e conectando-os no pipeline. E quando fizer isso, deve considerar cuidadosamente se esses módulos HTTP devem ser assíncronos.
A Figura 6 contém o código-fonte de um módulo HTTP simples, síncrono, chamado RequestLogModule, que registra solicitações de entrada em um arquivo de texto chamado RequestLog.txt. Ele cria o arquivo no diretório App_Data do site para que os usuários não possam navegar para ele. (Observe que a entidade de segurança executada pelo ASP.NET, como ASPNET ou Network Service, por exemplo, deve ter permissão de gravação para o App_Data para que isso funcione). O módulo implementa a interface IhttpModule, que é o único requisito de um módulo HTTP. Quando o módulo é carregado, seu método Init registra um manipulador para os eventos HttpApplication.PreRequestHandlerExecute, que são disparados a partir do pipeline em cada solicitação. O manipulador de eventos abre o RequestLog.txt (ou o cria caso ainda não exista) e o grava em uma linha de texto que contenha informações pertinentes sobre a solicitação atual, incluindo a hora e a data em que a solicitação chegou, o nome de usuário do solicitante, se a solicitação estiver autenticada (ou o endereço IP do solicitante se a autenticação estiver desativada), e a URL solicitada. O módulo é registrado na seção <httpModules> de web.config, solicitando que ele seja carregado pelo ASP.NET sempre que o aplicativo for iniciado.

Figure 6 Módulo HTTP síncrono
using System;
using System.Web;
using System.IO;
public class RequestLogModule : IHttpModule
{
public void Init (HttpApplication application)
{
application.PreRequestHandlerExecute +=
new EventHandler(OnPreRequestHandlerExecute);
}
public void Dispose () {}
void OnPreRequestHandlerExecute(Object sender, EventArgs e)
{
HttpApplication app = (HttpApplication)sender;
DateTime time = DateTime.Now;
using(StreamWriter writer = new StreamWriter(
app.Server.MapPath("~/App_Data/RequestLog.txt"), true))
{
string line = String.Format(
"{0,10:d} {1,11:T} {2, 32} {3}",
time, time,
app.User.Identity.IsAuthenticated ?
app.User.Identity.Name :
app.Request.UserHostAddress,
app.Request.Url);
writer.WriteLine (line);
}
}
}
O problema com RequestLogModule é duplo. Primeiro, ele realiza E/S de arquivo em cada solicitação. Segundo, ele usa um thread de processamento de solicitação para realizar a E/S: um thread que deveria ser usado para atender a solicitações de entrada adicionais. Este módulo, simples como é, causa uma penalidade na taxa de transferência. Embora você possa atenuar o atraso processando em lotes as operações de E/S do arquivo, uma abordagem melhor é tornar o módulo assíncrono (ou, melhor ainda, processar em lotes a E/S do arquivo e tornar o módulo assíncrono).
A Figura 7 mostra a versão assíncrona de RequestLogModule. Chamada de AsyncRequestLogModule, ela realiza exatamente o mesmo trabalho, mas retorna o thread atribuído à solicitação ao pool de threads antes de gravar no arquivo. Quando a gravação é concluída, um novo thread é obtido do pool de threads e usado para finalizar a solicitação.

Figure 7 Módulo HTTP assíncrono
using System;
using System.Web;
using System.IO;
using System.Threading;
using System.Text;
public class AsyncRequestLogModule : IHttpModule
{
private FileStream _file;
private static long _position = 0;
private static object _lock = new object();
public void Init (HttpApplication application)
{
application.AddOnPreRequestHandlerExecuteAsync (
new BeginEventHandler (BeginPreRequestHandlerExecute),
new EndEventHandler (EndPreRequestHandlerExecute)
);
}
IAsyncResult BeginPreRequestHandlerExecute (Object source,
EventArgs e, AsyncCallback cb, Object state)
{
HttpApplication app = (HttpApplication)source;
DateTime time = DateTime.Now;
string line = String.Format(
"{0,10:d} {1,11:T} {2, 32} {3}\r\n",
time, time,
app.User.Identity.IsAuthenticated ?
app.User.Identity.Name :
app.Request.UserHostAddress,
app.Request.Url);
byte[] output = Encoding.ASCII.GetBytes(line);
lock (_lock)
{
_file = new FileStream(
HttpContext.Current.Server.MapPath(
"~/App_Data/RequestLog.txt"),
FileMode.OpenOrCreate, FileAccess.Write,
FileShare.Write, 1024, true);
_file.Seek(_position, SeekOrigin.Begin);
_position += output.Length;
return _file.BeginWrite(output, 0, output.Length, cb, state);
}
}
void EndPreRequestHandlerExecute (IAsyncResult ar)
{
_file.EndWrite(ar);
_file.Close();
}
public void Dispose () {}
}
O que torna AsyncRequestLogModule assíncrono? Seu método Init chama HttpApplication.AddOnPreRequestHandlerExecuteAsync para registrar métodos de início e fim para eventos PreRequestHandlerExecute. A classe HttpApplication realiza outros métodos AddOn para outros eventos por solicitação. Por exemplo, um módulo HTTP pode chamar AddOnBeginRequestAsync para registrar manipuladores assíncronos de eventos BeginRequest. O método BeginPreRequestHandlerExecute de AsyncRequestLogModule usa o método FileStream.BeginWrite da estrutura para iniciar um gravação assíncrona. No momento em que BeginPreRequestHandlerExecute retorna, o thread volta para o pool de threads.
O AsyncRequestLogModule contém uma certa lógica de sincronização de threads que merece menção especial. É possível, e até mesmo provável, que várias solicitações em execução em vários threads serão gravadas no arquivo de log ao mesmo tempo. Para garantir que gravações simultâneas não se sobrescrevam, o AsyncRequestLogModule armazena a posição no arquivo para a próxima gravação em um campo particular compartilhado por todas as instâncias de módulo (_position). Antes de cada chamada para BeginWrite, o módulo lê a posição do campo e atualiza o campo para apontar para o primeiro byte seguinte ao que está para ser gravado no arquivo. Lógica que lê e atualiza:_position é inserido em uma instrução lock para que apenas um thread possa executá-lo por vez. Isso impede que um thread leia a posição antes que outro tenha a chance de atualizá-la.
Conclusão
A programação assíncrona é uma forma excelente de criar aplicativos mais escalonáveis usando o pool de threads do ASP.NET da forma mais eficiente possível. Em minhas viagens, eu raramente vejo desenvolvedores do ASP.NET utilizarem modelos de programação assíncrona, em parte porque eles simplesmente não sabem que esses modelos existem. Não deixe que a documentação esparsa o atrapalhe: comece a pensar de forma assíncrona hoje, e você estará criando aplicativos melhores amanhã.
Esteja ciente de que o código de exemplo transferível que acompanha este artigo vem nas versões C# e Visual Basic®. Eu recebo e-mails com freqüência pedindo versões em Visual Basic dos exemplos. Desta vez, não precisa pedir, eles já estão lá!
Envie suas dúvidas e seus comentários para Jeff em wicked@microsoft.com.
Jeff Prosise é um editor colaborador da MSDN Magazine e autor de vários livros, incluindo "Programming Microsoft .NET" (Microsoft Press, 2002). Ele também é co-fundador da
Wintellect, uma empresa de consultoria e treinamento de software especializada em .NET.