Introdução
Com a evolução do framework e a implementação do modelo Async/Await, fiquei curioso sobre como seria o desempenho em relação aos demais métodos de integração já apresentados nas demais partes desta série (Ver o Índice para localizar as demais).
O que muda na implementação?
Basicamente, peguei a mesma implementação usada na parte 4 para SOAP e NET.TCP e converti todas as chamadas, do cliente e do servidor para o modelo async/await. Na série sobre programação paralela (parte 4), expliquei no que consiste o modelo e a razão dos ganhos de desempenho apresentados por ele.
No código servidor, fizemos as seguintes alterações:
public List<ServiceTable> GetServiceTablesAsynchronous(int IDInicial, int IDFinal)
{
if (String.IsNullOrEmpty(connString))
GetConnString();
List<ServiceTable> l = new List<ServiceTable>();
Queue<Task<ServiceTable>> queue = new Queue<Task<ServiceTable>>();
for (int i = IDInicial; i <= IDFinal; i++)
queue.Enqueue(DAO.GetServiceTableAsync(connString, i));
while (queue.Count > 0)
{
Task<ServiceTable> task = queue.Dequeue();
task.Wait();
l.Add(task.Result);
}
return l;
}
A idéia do código acima é que cada chamada que vai no banco de dados é executada de forma assíncrona, sendo que a chamada subsequente não espera o término dela para fazer a próxima requisição ao banco. Por isso é usada uma Queue para guardar todas as tasks e aguardar o término delas antes de retornar a resposta a quem chamou o serviço externamente.
Óbvio que seria muito mais eficiente fazer a query, assíncrona com todo o lote de uma vez só, mas para manter o mesmo cenário utilizada nas 13 partes anteriores dessa série, utilizei essa abordagem.
A chamada ao banco, tem a seguinte implementação:
public static async Task<ServiceTable> GetServiceTableAsync(string ConnString, int ServiceTableID, TaskLoggingHelper Log)
{
ServiceTable result = new ServiceTable();
SqlConnection conn = new SqlConnection(ConnString);
conn.Open();
SqlCommand cmd = new SqlCommand("select ServiceTableID, DescServiceTable, Value, CreationDate, StringField1, StringField2 " +
"from ServiceTable where ServiceTableID = @ServiceTableID", conn);
using (conn)
{
SqlParameter p1 = cmd.Parameters.Add("@ServiceTableID", SqlDbType.Int);
p1.Value = ServiceTableID;
SqlDataReader rd = await cmd.ExecuteReaderAsync();
rd.Read();
using (rd)
{
result.ServiceTableID = rd.GetInt32(0);
result.DescServiceTable = rd.GetString(1);
result.Value = (float)rd.GetDouble(2);
result.CreationDate = rd.GetDateTime(3);
result.StringField1 = rd.GetString(4);
result.StringField2 = rd.GetString(5);
}
}
if (Log != null)
Log.LogMessage("Getting ServiceTableID: " + ServiceTableID.ToString());
return result;
}
A principal mudança no código foi “await cmd.ExecuteReaderAsync();”. Basicamente utilizando novamente o modelo assíncrono para cada cutucada no banco de dados.
No código do cliente, utilizamos o stub do WCF em sua versão assícrona. Basicamente, ao adicionar as referências, utilizamos as opções:
Os métodos gerados para o serviço WCF ganham o sufixo “Async” no final e os mesmos retornam Tasks, invés das tradicionais implementações síncronas.
O consumo deles foi escrito da seguinte maneira:
int count = 1;
Queue<Task<ServiceTable[]>> tasks = new Queue<Task<ServiceTable[]>>();
for (int i = 0; i < TotalBatches; i++)
{
tasks.Enqueue(client.GetServiceTablesAsynchronousAsync(count, count + (BatchSize - 1)));
count += BatchSize;
}
Queue<Task> queue2 = new Queue<Task>();
while (tasks.Count > 0)
{
Task<ServiceTable[]> task = tasks.Dequeue();
task.Wait();
ServiceTable[] stArray = task.Result;
foreach (ServiceTable t in stArray)
{
ServiceTable t2 = new ServiceTable();
t2.ServiceTableID = t.ServiceTableID;
t2.DescServiceTable = t.DescServiceTable;
t2.Value = t.Value;
t2.CreationDate = t.CreationDate;
t2.StringField1 = t.StringField1;
t2.StringField2 = t.StringField2;
queue2.Enqueue(DAO.ProcessServiceTableAsync(ConnString, t2));
}
}
while (queue2.Count > 0)
queue2.Dequeue().Wait();
A idéia é muito similar ao lado do servidor. Enquanto eu ainda não recebi a resposta para processar, continuo enviando requisições. Ao terminar de enviar todas as requisições, espero a primeira resposta, e vou processando o resultado de cada uma delas.
O nome do método GetServiceTablesAsynchronousAsync ficou bem estranho, porque minha falta de criatividade colocou o sufixo “Asynchronous” no nome do método no servidor (para diferenciar do outro método, síncrono) e ao gerar a chamada do método, o WCF adicionou o sufixo Async novamente.
O método que vai no banco de dados, do lado do cliente também foi implementado de forma assíncrona:
public static async System.Threading.Tasks.Task ProcessServiceTableAsync(string ConnString, ServiceTable table)
{
SqlConnection conn = new SqlConnection(ConnString);
conn.Open();
SqlCommand cmd = new SqlCommand("insert into ClientTable (ClientTableID, DescClientTable, Value, CreationDate, StringField1, StringField2)" +
"values (@ClientTableID, @DescClientTable, @Value, @CreationDate, @StringField1, @StringField2)", conn);
using (conn)
{
SqlParameter p1 = cmd.Parameters.Add("@ClientTableID", SqlDbType.Int);
SqlParameter p2 = cmd.Parameters.Add("@DescClientTable", SqlDbType.VarChar, 200);
SqlParameter p3 = cmd.Parameters.Add("@Value", SqlDbType.Float);
SqlParameter p4 = cmd.Parameters.Add("@CreationDate", SqlDbType.DateTime);
SqlParameter p5 = cmd.Parameters.Add("@StringField1", SqlDbType.VarChar, 200);
SqlParameter p6 = cmd.Parameters.Add("@StringField2", SqlDbType.VarChar, 200);
p1.Value = table.ServiceTableID;
p2.Value = table.DescServiceTable;
p3.Value = table.Value;
p4.Value = table.CreationDate;
p5.Value = table.StringField1;
p6.Value = table.StringField2;
await cmd.ExecuteNonQueryAsync();
}
}
O segredo aqui está em ExecuteNonQueryAsync. Isso significa que antes de receber a resposta do insert, já estou preparando o próximo insert.
A implementação toda tem por objetivo eliminar toda a espera entre cliente e servidor.
Tempos de execução
Como novamente tive mudanças no meu ambiente, fui obrigado a refazer os tempos. Como são muitos números e demora algumas preciosas horas reexecutá-los fiz apenas alguns métodos para conseguirmos ter uma relação de comparação com os demais métodos. A metodologia é a mesma. Cada teste é executado 10 vezes e o tempo apresentado aqui é a média.
Temos os seguintes tempos:
Como observamos, a implementação de NET.TCP ficou mais rápida que o meu antigo socket server multi thread! Até os métodos estudados, este era o mais rápido e mais complexo de implementar.
Isso significa que agora vou ter que reimplementar os servidores TCP puros novamente, utilizando dos mesmos benefícios do async/await. Quem sabe num post futuro?
Conclusão
A Microsoft fez um belo trabalho na implementação dessas API’s baseadas em async/await. Conseguiu tornar muito simples a aplicação prática de um conceito de difícil implementação.
Era isso!



