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!