Introdução
Uma das abordagens comumente usadas para diminuir o tempo total na integração é utilizar várias threads no consumo do serviço. O objetivo é compensar a espera por I/O paralelizando o processamento.
O objetivo desta parte da série (que eu nunca imaginava que chegaria na parte 16) é comparar este método com as demais abordagens.
Alterações no código
O servidor utilizado é o mesmo da parte 4, método síncrono normal. No cliente, tivemos algumas alterações:
public override bool Execute() { Binding binding; if (this.EndpointType == "http") { binding = new CustomBinding(); ((CustomBinding)binding).Elements.Add(new TextMessageEncodingBindingElement(MessageVersion.Soap11, Encoding.UTF8)); ((CustomBinding)binding).SendTimeout = new TimeSpan(0, 50, 0); ((CustomBinding)binding).ReceiveTimeout = new TimeSpan(0, 50, 0); ((CustomBinding)binding).OpenTimeout = new TimeSpan(0, 50, 0); ((CustomBinding)binding).CloseTimeout = new TimeSpan(0, 50, 0); HttpTransportBindingElement element = new HttpTransportBindingElement(); element.MaxReceivedMessageSize = 2048 * 1024; element.KeepAliveEnabled = false; element.RequestInitializationTimeout = new TimeSpan(1, 0, 0); ((CustomBinding)binding).Elements.Add(element); } else if (this.EndpointType == "nettcp") { binding = new NetTcpBinding(); ((NetTcpBinding)binding).MaxReceivedMessageSize = 1024 * 1024; ((NetTcpBinding)binding).Security.Mode = SecurityMode.None; ((NetTcpBinding)binding).CloseTimeout = new TimeSpan(0, 50, 10); ((NetTcpBinding)binding).OpenTimeout = new TimeSpan(0, 50, 10); ((NetTcpBinding)binding).ReceiveTimeout = new TimeSpan(0, 50, 10); ((NetTcpBinding)binding).SendTimeout = new TimeSpan(0, 50, 10); } else throw new ArgumentException("Invalid value for EndpointType. Expected: http, nettcp"); EndpointAddress address = new EndpointAddress(new Uri(WebServiceUri)); IntegrationTestsService.IntegrationTestsServiceClient client = new IntegrationTestsService.IntegrationTestsServiceClient(binding, address); Log.LogMessage("Doing " + TotalBatches.ToString() + " batch calls with " + BatchSize.ToString() + " itens each"); Stopwatch watch = new Stopwatch(); watch.Start(); int count = 1; ConcurrentQueue<ManualResetEvent> waitObjectQueue = new ConcurrentQueue<ManualResetEvent>(); Task task = null; for (int i = 0; i < TotalBatches; i++) { int start = count; int end = count + (BatchSize - 1); count += BatchSize; if (UseTask) { task = Task.Factory.StartNew(() => { ThreadJob(client, waitObjectQueue, start, end); }); } else { Thread thread = new Thread(() => { ThreadJob(client, waitObjectQueue, start, end); }); thread.Start(); } } if (task != null) task.Wait(); while (waitObjectQueue.Count > 0){ ManualResetEvent e; if (waitObjectQueue.TryDequeue(out e)) e.WaitOne(); } watch.Stop(); Log.LogMessage("Total processing time: " + watch.Elapsed.TotalSeconds.ToString("0.00") + " seconds"); return true; } private void ThreadJob(IntegrationTestsService.IntegrationTestsServiceClient client, ConcurrentQueue<ManualResetEvent> waitObjectQueue, int start, int end) { ManualResetEvent e = new ManualResetEvent(false); waitObjectQueue.Enqueue(e); ServiceTable[] stArray = client.GetServiceTables(start, end); 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; DAO.ProcessServiceTable(ConnString, t2); } e.Set(); }
A propriedade UseTask, determina se o trabalho será feito numa Task ou Thread. A diferença entre usar uma task ou uma thread é explicada no post programação paralela – parte 1. Na prática, para este exemplo, vemos que não fará muita diferença na prática.
O objetivo do código é simples. Cada lote de requisições (mesmo cenário utilizado em todas as partes dessa série) é executada numa thread ou task separada. Teremos por um lado o ganho de não esperar a requisição terminar para começar outra e pelo outro o custo da troca de contexto e alocação de memória em threads (explicados na parte 2 e parte 3 da série sobre programação paralela. Outro ponto é a sobrecarga por mais processamento paralelo gerado no servidor.
Resultados
Abaixo seguem os resultados desta abordagem, executada tando em SOAP quanto em NET.TCP. O mesmo método é utilizado para computar os tempos, ou seja, são feitas 10 execuções e o tempo apresentado abaixo é a média deles.
Como os números mostram, o uso de threads ou tasks não faz praticamente nenhuma diferença neste caso. Minha melhor explicação para isso é que o TaskScheduler utilizado para delegar as tasks abre uma nova thread sempre que identifica que as demais estão em espera (ninguém está consumindo CPU). Como existe muito bloqueio de espera por I/O num cenário de integração destes, a Task Parallel Library acaba abrindo uma nova thread quase sempre.
Os números também mostram que o uso da abordagem async/await (parte 15 da série de integrações e parte 4 da série de programação paralela) é ~53% mais eficiente em SOAP e ~39% mais eficiente em NET.TCP.
Código fonte
O código fonte dos exemplos está atualizado no Git Hub: https://github.com/ericlemes/IntegrationTests
Conclusão
O aproveitamento do tempo de espera por operações de I/O mostra ser a melhor maneira de aumentar o desempenho das aplicações. A implementação do padrão async/await no framework .NET, cada vez mais mostra ser uma forma extremamente simples de atingir este objetivo. Realmente é um trabalho fantástico por parte da Microsoft.