Integrações entre Sistemas – Parte 14 – Web Service (Async)

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:

ServiceRef1

ServiceRef2

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:

Comparativo1

Comparativo2

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!

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