Integrações entre Sistemas – Parte 16 – Consumo de Web Services Multi-Thread

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.

pt16-Comparativo1

pt16-Comparativo2

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.

Advertisement

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s