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.

