Categoria: .Net

Integrações entre Sistemas – Parte 15 – Web Service (Async) com Servidor Síncrono

Introdução

Após uma conversa interessante com o Roger Luiz Pereira (grande desenvolvedor e amigo de trabalho) no escritório, alguns questionamentos interessantes foram levantados sobre os métodos.

Um deles está relacionado com o quanto de desempenho a implementação com async/await no lado do servidor contribui no desempenho. O objetivo deste post é contemplar este cenário nos trabalhos.

Método

Para testar esse cenário, utilizei exatamente a mesma implementação do cliente utilizada na parte 14 para o lado do cliente, com um parâmetro a mais para utilizar o servidor síncrono (método GetServiceTablesAsync), invés do servidor assíncrono (GetServiceTablesAsynchronousAsync). A implementação síncrona é a mesma utilizada na parte 4.

Resultados

A mesma metodologia é usada nestes números. Os testes são executados 10 vezes e o resultado apresentado aqui é a média dos 10. Os resultados seguem na tabela abaixo:

pt15-Comparativo1

pt15-Comparativo2

Conclusão

Como vemos, a implementação do servidor assíncrona trás benefícios tangíveis no desempenho total. Para NET.TCP: ~37% mais rápido e para SOAP ~43% mais rápido.

Código-fonte

O código-fonte de todos os exemplos está atualizado no Git Hub: https://github.com/ericlemes/IntegrationTests

Anúncios

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!

Programação Paralela – Parte 4 – I/O Assícrono com Async/Await

Introdução

Na parte 3 desta série, vimos como o I/O assíncrono pode ser mais eficiente, liberando a CPU da thread que espera por I/O para fazer outra atividade, aumentando o desempenho geral da aplicação.

Vimos também como essa implementação pode ser complexa. O objetivo deste post é mostrar como as novas features de async/await, implementadas no framework 4.5 podem simplificar este processo.

Utilizando o async/await

No exemplo anterior, utilizamos um método que faz uma grande escrita em disco, seguida de um grande processamento. Vamos recriar este exemplo, utilizando o padrão async/await.

        [TestMethod]
        public void BigAsyncIOTest2()
        {
            Stopwatch watch = new Stopwatch();
            watch.Start();
            
            for (int i = 0; i &lt; 10; i++)            
                DoBigWriteAsync().Wait();
            
            watch.Stop();
            Console.WriteLine(&quot;Total elapsed (ms): &quot; + watch.ElapsedMilliseconds);
        }

        private async Task DoBigWriteAsync()
        {
            FileStream fs = new FileStream(&quot;file1.txt&quot;, FileMode.Create, FileAccess.ReadWrite);
            byte[] buffer = new byte[100 * 1024 * 1024]; //Não faça isso. Aloca 100Mb de memória.                        
            Task t = fs.WriteAsync(buffer, 0, buffer.Length);           
            Task t2 = t.ContinueWith(new Action((task) =&gt; { 
                fs.Flush();
                fs.Close();
            }));
            BubbleSort.DoBigBubbleSort(10000, null);
            await t;            
        }

O método DoBigWriteAsync teve um modificador “async” iniciado e um “await t” inserido no final. O método WriteAsync implementado pela classe Stream, provê os mesmos mecanismos. Na verdade em nível mais baixo da API é ele quem faz o tratamento da chamada de I/O assícrono.

Na prática, o que acontece nesta implementação é que cada vez que WriteAsync é chamado, uma continuação é inserida (para dar o Flush e o Close na Stream) e o BubbleSort é iniciado. Somente quando ocorre o “await t”, é aguardado o término do I/O para que o método devolva o resultado.

Na prática, o tempo que o método ficou parado com “await” não travou a thread da aplicação e sim a disponibilizou para executar outras atividades enquanto o I/O está em andamento. Aí está toda a mágica e a simplicidade do async/await. Você escreve um método com a mesma característica e simplicidade que escreve um método sícrono, porém, o framework não travará a thread.

Qual o desempenho?

Pelos testes muito simples executados nesta série, podemos perceber que há um overhead em relação ao método assíncrono puro (utilizado na parte 2 da série). O método executou em 13835 milisegundos, contra 9111 milisegundos da implementação pura. Meu palpite é que este overhead ocorre devido à mecânica da Task Parallel Library que exploraremos em seguida.

Quais as vantagens de usar?

Praticamente todas as API’s de I/O no framework estão sendo reescritas para suportar este padrão, entre elas: Acesso HTTP, Sockets, Web Services, Banco de dados (DataReader), etc. A lista completa está em: http://msdn.microsoft.com/pt-br/library/hh191443.aspx.

Você pode fazer, com baixo esforço, sua aplicação sofrer muito menos com problemas de responsividade e espera por I/O. Somente no exemplo acima, partimos de 20 segundos numa implementação totalmente síncrona para 14 segundos, basicamente trocando keywords nos métodos.

Como funciona?

Sempre que um método assíncrono (Ex.: WriteAsync) é chamado, o mesmo inicia a operação de I/O e cria uma Task que somente conclui quando o retorno do callback avisando que a operação de I/O terminou. Ou seja, quando o callback termina, a task é marcada como concluída. Nenhuma nova thread (worker thread) é criada neste processo. Este é um dos pontos que mais me gera confusão, pois apesar da classe Task fazer parte do namespace System.Threading, não significa que sempre que uma task é criada a mesma é executada numa thread separada.

Pelo próprio Task Parallel Library, toda nova task, é criada numa TaskFactory padrão, que utiliza um TaskScheduler padrão. O TaskScheduler padrão do framework é um ThreadPoolTaskScheduler. Ele tem um comportamento que a cada nova Task ele delega para seu thread pool a sua execução. Em outras palavras, ele tenta utilizar alguma thread livre do seu thread pool. Se não possui nenhuma, cria uma. Se possui threads demais (maior que a quantidade de cores da máquina), ele aguarda alguma task finalizar e pega uma das threads já criadas.

Com este comportamento, ele consegue manter o maior paralelismo possível, com a menor quantidade de trocas de contexto possível. Esperto, né?

Esse comportamento também proporciona um outro comportamento desagradável que é o fato de não sabermos exatamente em que thread a Task será executada, o que pode trazer potenciais problemas de locks, deadlocks, ou problemas com código não thread-safe sendo executado dentro de uma task. É o preço.

Por isso o overhead imposto acima. Numa execução simples, com a máquina com pouco processamento (cenário deste teste), há um incremento do tempo devido ao overhead imposto pelo processo de escalonamento das tasks. Num cenário de alta carga, o task scheduler deve valer seu preço.

Em contrapartida, sempre que executamos uma operação de I/O assíncrono no modelo async/await, esta task é enfileirada no TaskScheduler. Se ela está aguardando conclusão, a thread que originalmente estaria esperando pega outra Task e a executa. Essa outra task pode ser outra operação de I/O, um pedaço de processamento paralelo utilizado numa expressão PLINQ (parallel LINQ) ou qualquer outro processamento do framework que esteja encapsulado numa task.

Era isso.

Programação Paralela – Parte 3 – Operações de I/O e Locks

Introdução

Na parte 1 desta série, observamos que muitas vezes aumentar a quantidade de threads da aplicação sem nenhum critério pode causar pior desempenho do que manter a aplicação numa única thread. Na parte 2, como os locks também podem gerar problemas potenciais de performance. E qual a relação destes dois assuntos com I/O (input/output ou entrada/saída)?

O objetivo deste post é explicar esta relação.

O que são operações de I/O

Sempre que uma aplicação acessa algum recurso que está em algum periférico do computador (fora da CPU ou fora da memória), uma operação de I/O é realizada. Ou seja, praticamente tudo. Operações de I/O ocorrem o tempo todo nas aplicações.

Exemplos de operações de I/O são acesso a disco e acesso a rede. Se acesso a rede é uma operação de I/O, obviamente tudo que implica comunicação, seja com banco de dados utilizando o driver do fornecedor, requests HTTP, TCP, UDP, ou seja, qualquer coisa que sai para rede.

Operações de I/O síncronas geram espera nas threads

Sempre que uma operação de I/O é feita, há uma espera na thread. Explicando de uma forma mais detalhada, se sua aplicação faz um acesso a disco de forma síncrona, no momento em que o sistema operacional vai no disco, a thread está parada esperando o cabeçote do HD se mover até o local correto (seek time), realizar a leitura e devolver as informações. Existem diversos mecanismos de cache para minimizar o movimento físico do HD, mas usei um exemplo forte para que seja possível imaginar a razão da lentidão.

Em máquinas modernas esse tempo é praticamente imperceptível para humanos (fica nos milisegundos), mas do ponto de vista do computador, são vários ciclos de CPU desperdiçados que poderiam estar sendo usados para outras operações enquanto a CPU está aguardando a resposta do periférico.

Um exemplo de I/O síncrono:

        public void SynchronousIOTest()
        {
            FileStream fs = new FileStream("file1.txt", FileMode.Create, FileAccess.ReadWrite);            
            byte[] buffer = new byte[100 * 1024 * 1024]; //Não faça isso. Aloca 100Mb de memória.            
            fs.Write(buffer, 0, buffer.Length);
            fs.Flush();
            fs.Close();
        }

A idéia do código acima é que no “Write”, a thread fica esperando o disco escrever 100Mb antes de continuar.

Isso executa bem rápido. Na minha máquina demorou ~1 segundo. É válido lembrar que em servidores, com diversas aplicações acessando o disco simultaneamente, isso pode piorar significativamente.

A mesma idéia, de forma assíncrona.

        public void AsyncIOTest()
        {
            FileStream fs = new FileStream("file1.txt", FileMode.Create, FileAccess.ReadWrite);
            byte[] buffer = new byte[100 * 1024 * 1024]; //Não faça isso. Aloca 100Mb de memória.            
            fs.BeginWrite(buffer, 0, buffer.Length, null, null);
            fs.Flush();
            fs.Close();
        }

A única coisa que mudou foi o “BeginWrite” no lugar do Write. O tempo cai para 10ms. Óbvio que a implementação acima está errada, mas ela serve para ilustrar que no primeiro exemplo, a thread espera toda a operação de I/O enquanto no segundo exemplo, ela continua a execução mesmo que a operação de I/O não esteja completa.

Numa implementação correta de I/O assíncrono deveria haver o tratamento do callback. Na verdade, a API de I/O “avisa” sua aplicação quando a operação de I/O é concluída, sendo que entre o BeginWrite e a chamada do callback, você pode usar a CPU para o que bem entender.

Em outras palavras, a espera por I/O funciona exatamente como um lock na thread. No exemplo acima, usei uma escrita em disco, mas uma escrita em rede funciona exatamente da mesma maneira. Sim, toda vez que você executa o seu SqlCommand, ou enche o seu DataSet com dados, ou chama seu querido Entity Framework, NHibernate, por debaixo dos panos, essa espera por I/O está acontecendo e está travando a thread (ressalvas para quem já utiliza as novas API’s de I/O).

Afinal, como funciona o I/O assíncrono?

É uma excelente pergunta. É algo que funciona muito próximo de magia negra. A explicação simples é que sempre que ocorre uma operação de I/O, a CPU pede para o periférico realizar a operação e continua processando. CPU’s são máquinas sequenciais e não param nunca. CPU ociosa é CPU jogada fora.

Quando o periférico termina de fazer o que tem que fazer, ele gera uma interrupção (quem já configurou jumper na unha, já ouviu falar de IRQ), avisando a CPU que ele terminou.

Essencialmente, o Windows encapsulou muito bem isso numa API chamada I/O completion ports. Essa API tem a idéia de permitir que, lá do nível dos dispositivos, passando pelos drivers (kernel mode) até o código que executa em user mode, a thread que originalmente pediu a operação de I/O receba uma notificação de que ela terminou.

O post do Stephen Cleary “there is no thread” explica passo a passo como isso acontece, sofrendo diversos “empréstimos” de threads existentes até notificar a thread original. O ponto que ele detalha é que não existe uma worker thread parada, esperando a operação de I/O terminar. A thread fica executando até que seja notificada pelo sistema operacional que o I/O terminou.

Esperto, né?

Dá muita diferença?

Para exemplificar a diferença entre o I/O síncrono e assíncrono, montei o exemplo abaixo. A idéia é realizar 10 vezes um processamento que combina o uso de recursos de CPU (de novo o grande e gastão BubbleSort) com I/O.

Exemplo síncrono:

        [TestMethod]
        public void BigSyncIOTest()
        {
            Stopwatch watch = new Stopwatch();
            watch.Start();

            for (int i = 0; i < 10; i++)
            {                
                FileStream fs = new FileStream("file1.txt", FileMode.Create, FileAccess.ReadWrite);
                byte[] buffer = new byte[100 * 1024 * 1024]; //Não faça isso. Aloca 100Mb de memória.            
                fs.Write(buffer, 0, buffer.Length);
                fs.Flush();
                fs.Close();

                BubbleSort.DoBigBubbleSort(10000, null);
            }

            watch.Stop();
            Console.WriteLine("Total elapsed (ms): " + watch.ElapsedMilliseconds);
        }

O exemplo síncrono é realmente bem simples. Escreve 100Mb em disco, depois faz um bubble sort numa matriz invertida de 10000 elementos.

O exemplo assíncrono é mais complexo (implementado desta maneira propositalmente com o APM para fins didáticos). O método BeginWrite recebe dois parâmetros adicionais, um AsyncCallback e um object para manter estado.

A idéia deste modelo de programação é que o método passado como callback é chamado quando a operação de I/O termina.

A classe utilizada para manter estado, segue essa estrutura:

    public class State
    {
        public int Current
        {
            get;
            set;
        }

        public FileStream FileStream
        {
            get;
            set;
        }
    }

O método principal:

        private ManualResetEvent ev;

        [TestMethod]
        public void BigAsyncIOTest()
        {
            Stopwatch watch = new Stopwatch();
            watch.Start();

            ev = new ManualResetEvent(false);

            WriteFinishedCallback(null);
            ev.WaitOne();

            watch.Stop();
            Console.WriteLine("Total elapsed (ms): " + watch.ElapsedMilliseconds);
        }

        public void WriteFinishedCallback(IAsyncResult ar)
        {
            State state = null;
            if (ar == null)
            {
                state = new State();
                state.Current = 0;
            }
            else{
                state = (State)ar.AsyncState;
                state.FileStream.EndWrite(ar);
                state.FileStream.Flush();
                state.FileStream.Close();
                state.Current++;
                if (state.Current == 10)
                {
                    ev.Set();
                    return;
                }
            }            

            FileStream fs = new FileStream("file1.txt", FileMode.Create, FileAccess.ReadWrite);
            byte[] buffer = new byte[100 * 1024 * 1024]; //Não faça isso. Aloca 100Mb de memória.            
            fs.BeginWrite(buffer, 0, buffer.Length, WriteFinishedCallback, state);
            state.FileStream = fs;

            BubbleSort.DoBigBubbleSort(10000, null);

        }      

Vejam que um objeto de sincronização é necessário. Ele será acionado de dentro do método WriteFinishedCallback, que será chamado da primeira vez do método principal e será chamado recursivamente até completar 10 vezes. Realmente é bem chatinho implementar esse modelo e debugar então, fora de questão. Debugar esse tipo de código assíncrono é uma tarefa das mais ingratas.

O “payload” do processamento é o mesmo. O mesmo bubble sort gastão, e a mesma escrita em disco. Os resultados:

  • Síncrono: 21781 milisegundos
  • Assíncrono: 9111 milisegundos

A explicação é simples. No modelo assíncrono, enquanto a CPU espera I/O ela fica ordenando a matriz. No modelo síncrono, ela espera toda a operação de I/O terminar para então começar a processar o bubble sort. Fica menos da metade do processamento total, utilizando uma única thread.

Já imaginou isso escalado para toda uma aplicação, fortemente orientada a banco de dados?

Programação Paralela – Parte 2 – Locks em threads

O que é um lock?

Há situações em threads que precisamos fazer com que uma thread espera a outra, dado que ambas pretendem acessar um recurso que não pode ser acessado simultaneamente. Neste caso, utilizamos locks.

O problema é que sempre que uma thread espera por um lock, ela está consumindo recursos, gerando trabalho para o escalonador do sistema operacional e não está fazendo nada. Esse comportamento pode gerar problemas de desempenho.

Locks

Como explicado no post anterior (Programação paralela – Parte 1), threads compartilham o heap. Dessa maneira, eventualmente queremos evitar que as threads concorram pelos mesmos recursos. Para isso, existem objetos de sincronização, como no exemplo abaixo:

public void DoSomething()
{
    lock(this)
    {
         //Trecho que executará somente com uma thread de cada vez.
    }
}

O trecho protegido por lock, fará com que as threads não entrem simultaneamente no mesmo trecho. Existem diversos outros objetos de sincronização, inclusive que impedem que mais de um processo acesse o mesmo recurso simultaneamente. Eles ficam no namespace System.Threading.

Corrida de saco

corridadesaco

O grande problema de usarmos uma infinidade de threads com diversos pontos de sincronização (locks) é que nossas threads ficarão realizando uma espécie de corrida de sacos. Uma anda um pouquinho, mas precisa esperar a outra liberar um recurso, fazendo com que na média, tudo fique mais lento.

Para exemplificar, alterei o exemplo anterior, a parte que cada uma das threads executa, para o seguinte código:

        private void DequeueAndProcess(Queue<Job> queue)
        {
            while (true)
            {
                Job job = null;
                lock (queue)
                {
                    if (queue.Count > 0)
                        job = queue.Dequeue();

                    if (job == null)
                    {
                        Thread.Sleep(0);
                        continue;
                    }
                    job.JobDelegate();
                    job.JobEndedNotifier.Set();
                }
            }
        }

Neste caso, obtemos os seguintes tempos:

ProcessamentoParalelo-pt2-1

O exemplo é bastante exagerado, pois todo o processamento está sendo serializado dado que somente uma thread consegue processar o trecho protegido pelo lock, mas para fins didáticos, nos ajuda a entender porque muitas vezes o processamento multi-thread concorrendo por vários recursos fica pior do que o processamento single-threaded. A reposta é que além do custo do processamento, temos todo o custo de gerenciar as threads. Como desenvolvedor, não sentimos este custo, mas o sistema operacional sem dúvida sente.

Deadlocks

deadlock

Se deadlocks em bancos de dados incomodam, deadlocks em threads fazem você sentir saudades dos de banco de dados. O processo é o mesmo: duas threads que dependem de mais de um recurso tentam os obter forma inversa. O sintoma é bem diferente: as threads envolvidas no deadlock simplesmente congelam. Diferentemente do banco de dados, não existe um recurso que automaticamente escolhe uma thread como vítima, e automaticamente a mata. Você precisa desenvolver este mecanismo por sua conta.

Por sorte, deadlocks em threads são tão raros quanto um deadlock na vida real, mas como a foto acima ilustra, eles acontecem.

I/O

Como regra geral, handles também são recursos que não suportam concorrência. O mesmo mecanismo de forçar locks deve ser utilizado para evitar que duas threads tentem escrever no mesmo socket ao mesmo tempo, tentem escrever no mesmo arquivo ao mesmo tempo e assim sucessivamente. Em geral, utiliza-se mecanismos como o “lock” exemplificado acima.

Tipos thread-safe

Outra preocupação que devemos ter ao utilizar concorrência por objetos entre threads está relacionada ao tipo de dados utilizado. No MSDN há para cada tipo de dados observações sobre o tipo de dado ser thread-safe ou não (as vezes notas bastante confusas!).

Em geral, um tipo thread safe é aquele que você não precisa ter medo de acessar ou modificar em diferentes threads (Ex.: ConcurrentQueue). Tipos não thread-safe, você precisa explicitamente utilizar um lock (Ex.: Queue), o que pode gerar gargalos de desempenho, como apresentado anteriormente.

Notas importantes para aplicações Web

Aplicações web são em geral, multi-threaded sem muitas vezes nós percebermos. Geralmente cada novo request é servido numa nova thread, ou seja, se 10 usuários pedem para um servidor servir um request, é possível que tenhamos 10 threads no servidor. Utilizo as palavras geralmente e “é possível” porque o ASP.NET utiliza um mecanismo interno dele para determinar quando ou não utilizar uma nova thread, justamente para evitar contenções e baixo desempenho, conforme apresentado nesses posts. As vezes é melhor fazer um usuário esperar e os 10 que estão sendo atendidos serem bem atendidos, do que deixar 11 com péssima responsividade.

Sendo que aplicações web são essencialmente multi-thread, as mesmas preocupações devemos ter em relação a locks. Se dois requests tentam escrever num arquivo simultaneamente, um dos dois tomará uma exception. Se há alguma estrutura em memória compartilhada entre os requests (exemplos: singletons, instâncias únicas de serviço do Spring.NET, dados estáticos em classes), os mesmos problemas de concorrência estão sujeitos a acontecer.

Conclusão

Como diria um amigo: pensou que ia ser fácil, começou errado!

Processamento paralelo está longe de ser simples. É um conceito um tanto quanto complexo e na prática, debuggar aplicações multi-thread é algo bastante complexo. A própria existência do debugger, de uma linha de log, pode fazer com que as threads concorram em locais diferentes e o bug não se apresente.

Mas o recado mais importante é que não adianta pegar uma aplicação que não foi feita para ter paralelismo, que possui diversos pontos de compartilhamento de informações e sair gerando threads e entupindo de locks onde os problemas são apresentados. Muitas vezes esse processo torna a aplicação ainda menos eficiente do que era quando totalmente single-threaded.

É importante pensar numa arquitetura que suporte concorrência e escala.

Programação paralela – Parte 1 – Quantas threads?

Introdução

Recentemente, comecei a fazer alguns estudos referentes à parte de programação assíncrona do .NET 4.5. Para chegar no conceito de como funciona a parte de I/O assíncrono, achei interessante voltar um pouco e começar pelas raízes da programação paralela, com o objetivo de ilustrar claramente as diferenças e como esses conceitos se combinam. Daí nasceu essa série.

Nesse post abordarei o conceito básico de threads, trocas de contexto e como isso pode resultar em desempenho pior do que aplicações que não utilizam nenhum processamento paralelo.

Afinal, o que é uma thread?

Antes de explicar uma thread, acho melhor explicar primeiro o que é um processo. Um processo nada mais é do que uma instância de um executável, na memória do computador, ou seja, cada aplicação ou serviço executam num processo. Basta ver a lista do seu task manager. Lá você tem uma lista de processos. Se você executa duas vezes a mesma aplicação, tem 2 processos executando, apesar do executável ser o mesmo.

Um processo tem suas próprias áreas de memória, seu stack e seu heap (no caso do .NET, managed heap), ou seja, um processo não invade a área de memória do outro processo. Quem vem de .NET não sabe muito bem o que isso significa na prática, mas quem vem de linguagens não gerenciadas (como C++) sabe bem do que eu estou falando. Se por alguma razão, fizer um deslocamento que dá numa região da memória utilizada por outro processo, seu processo capota (ainda bem).

Então o que é uma thread? Eu costumo explicar uma thread como “uma linha de execução separada” dentro de um mesmo processo. Neste caso, o heap é compartilhado por todas as threads do processo, e cada thread tem sua stack separada. De forma bem simplista, isso significa que se você criar uma instância de uma classe e acessá-la de duas diferentes threads, elas estarão no mesmo espaço de memória o que pode gerar efeitos indesejados em sua aplicação, porém as variáveis locais utilizadas não serão compartilhadas. O post Tipos Escalares e Tipos Complexos ou Tipos por Valor e Referência pode ajudar no entendimento destes conceitos.

O básico da idéia de threads está em:

        public void DoSomething()
        {
            //Algum processamento.
        }

        public void DoSomethingInParallel()
        {
            Thread t = new Thread(DoSomething);
            t.Start();
            DoSomething();
        }

No exemplo acima, ao executar “DoSomethingInParallel”, sua máquina executará duas vezes, simultaneamente, o método “DoSomething”, uma na thread principal (sempre que um novo processo inicia, ele tem uma thread principal) e outra numa thread secundária criada ao executar new Thread, passando como parâmetro um delegate do tipo ThreadStart criado a partir do método DoSomething.

Qual é o limite de threads?

Aqui as coisas começam a ficar complicadas. Não há um limite exato de threads, ou seja, pode-se criar centenas de threads num computador. Talvez a pergunta mais apropriada seja: até onde é eficiente paralelizar um processamento?

Não há uma resposta exata para essa pergunta, mas, existem algumas informações que podem nos ajudar a chegar nesse número mágico.

O limite máximo para execução em paralelo numa máquina está relacionado com o número de cores do processador da máquina, ou seja, numa máquina de 4 cores, 4 threads são executadas em paralelo. As demais, o sistema operacional acaba realizando escalonamento do processamento, ou seja, deixa uma executar um pouquinho, passa para a outra e assim sucessivamente, da mesma forma que ele faz com os processos.

O grande problema é que esse escalonamento é o que chamamos de troca de contexto, ou seja, toda vez que uma thread vai executar, o sistema operacional precisa pegar o stack daquela thread (que pode ter estar fora do cache e até mesmo em disco), recolocar na memória e reexecutar essa thread. Isso obviamente tem um custo.

A resposta para a pergunta acima é: É eficiente deixar o número de threads equivalente ao número de cores processando, sem que haja espera nessas threads. O que chamamos de espera é: espera por carregar itens da memória, operações de I/O, etc. Abordarei esse tema em mais detalhe (espera) em posts futuros.

Um pouco de prática

Para ilustrar o funcionamento das threads, criei um exemplo. A idéia é processar 400 vezes um bubble sort de uma matriz de 1000 itens. A idéia do código é criar worker threads que ficam esperando um determinado dado numa fila em memória, sempre que um dado destes é colocado, ela processa este dado, até que o trabalho todo termine. O código abaixo é o código executado por cada thread.

        private void DequeueAndProcess(ConcurrentQueue<Job> queue)
        {
            while (true)
            {
                Job job = null;
                if (!queue.TryDequeue(out job))
                {
                    Thread.Sleep(0);
                    continue;
                }
                job.JobDelegate();
                job.JobEndedNotifier.Set();
            }
        }        

Cada Job acima é uma estrutura que recebe um JobDelegate (delegate que faz o trabalho quando executado) e um JobEndedNotifier (ManualResetEvent utilizado para avisar o método que cria as threads que o trabalho terminou). Usei uma queue sincronizada justamente para evitar contenção (espera) entre as threads.

Em seguida, um método que cria um número de threads, mede o tempo de execução (Stopwatch) e aguarda o término da execução de todo o trabalho.

        private void CreateJobsAndProcessInParallel(int numberOfThreads)
        {
            ConcurrentBag<int> threadIDs = new ConcurrentBag<int>();
            Stopwatch watch = new Stopwatch();
            watch.Start();
            ConcurrentQueue<Job> queue = new ConcurrentQueue<Job>();
            List<Thread> list = new List<Thread>();
            List<ManualResetEvent> eventList = new List<ManualResetEvent>();
            for (int i = 0; i < numberOfThreads; i++)
            {
                Thread t = new Thread(() => { DequeueAndProcess(queue); });
                list.Add(t);
                t.Start();
            }
            for (int i = 0; i < numberOfJobs; i++)
            {
                ManualResetEvent e = new ManualResetEvent(false);
                eventList.Add(e);
                ThreadStart t = new ThreadStart(() =>
                {
                    BubbleSort.DoBigBubbleSort(bubbleSize, threadIDs);
                });
                queue.Enqueue(new Job(t, e));
            }
            foreach (ManualResetEvent e in eventList)
                e.WaitOne();
            foreach (Thread t in list)
                t.Abort();

            watch.Stop();
            Console.WriteLine("Total elapsed (ms): " + watch.ElapsedMilliseconds);
        }

O método DoBigBubbleSort apenas executa o bubble sort e guarda uma lista de quantas diferentes threads foram usadas no processamento.

Em seguida, executei o exemplo acima com 1, 2, 4, 100, 200, 400 threads, na minha máquina que possui 4 cores (2 cores físicos, 4 processadores lógicos). Os resultados estão abaixo:

ProcessamentoParalelo-pt1-2

ProcessamentoParalelo-pt1-1

Este gráfico nos mostra algumas características muito interessantes:

  • O processamento com duas threads é mais do que a metade do processamento com uma thread só. Na prática isso significa que apesar de paralelizar o processamento, a um custo para gerenciar as threads, além dos demais processos que executam no sistema operacional
  • O processamento com 4 threads é um pouquinho melhor do que o processamento com 2 threads, apesar da máquina ter somente 2 cores físicos. Acredito que esse número seja explicável com a existência de 4 processadores lógicos na máquina (ver hyper threading).
  • O mais interessante aqui é a partir de algo entre ~150 threads o processamento paralelo começa a ficar pior do que o processamento numa thread só. Isso é uma demonstração prática do custo de troca de contexto entre threads

Usando Task Parallel Library

Um outro exemplo, usando a TPL (Task Parallel Library):

        public void TaskBasedTest()
        {
            ConcurrentBag<int> threadIDs = new ConcurrentBag<int>();
            Stopwatch watch = new Stopwatch();
            watch.Start();
            List<Task> l = new List<Task>();
            for (int i = 0; i < numberOfJobs; i++)
                l.Add(Task.Factory.StartNew(() => { BubbleSort.DoBigBubbleSort(bubbleSize, threadIDs); }));
            foreach(Task t in l)
                t.Wait();
            watch.Stop();
            Console.WriteLine("Total elapsed (ms): " + watch.ElapsedMilliseconds);
            Console.WriteLine("Number of threads used: " + threadIDs.Distinct().Count());
        }

O exemplo acima, teve o seguinte tempo de execução:

ProcessamentoParalelo-pt1-3

Como vemos no código, não fazemos qualquer controle do número de threads. Apenas disparamos a execução e contamos quantas diferentes threads foram utilizadas no processamento. No exemplo acima, foram 5.

Como a TPL funciona? Qual é a mágica?

Sempre que executamos um Task.Factory.StartNew, estamos criando uma nova Task na TaskFactory “default”. A TaskFactory default utiliza uma implementação interna de um TaskScheduler (é uma classe abstrata). A implementação default é um ThreadPoolTaskScheduler.

Internamente, o ThreadPoolTaskScheduler delega a execução das tasks para um thread pool que procura manter sempre executando o número de threads correspondente ao número de processadores da máquina. Na prática quando recebe uma task, baseada em quantas threads estão executando, ele checa se deve ou não iniciar uma nova thread ou utilizar uma das que estão ociosas. Espertinho, não?

Na prática, quando usamos PLINQ (Parallel LINQ), Parallel.For, e outros, estamos delegando para este TaskScheduler a lógica de como quebrar o processamento em múltiplas threads, como visto no exemplo acima, um processo um tanto quanto complexo, que a API consegue nos abstrair de uma forma bem eficiente.

Código fonte do exemplo

O código ainda está uma bagunça, mesmo assim, subi para o GitHub: https://github.com/ericlemes/ParallelProgramming.

Linguagens compiladas, interpretadas e byte-code

Introdução

Recentemente, comecei a aprender Python. Tudo começou num bate-papo no café do TDC 2013, com o @juanplopes (thanks!). Estávamos discutindo sobre linguagens, ele mencionou o Python e eu imediatamente falei sobre minha “birra” com linguagens interpretadas. Então o Juan comentou de algumas alternativas de JIT-Compiler para o Python.

Esse post nasceu para falar de duas coisas: 1) Minha birra, 2) JIT-Compiler para o Python.

Um resuminho sobre linguagens compiladas, interpretadas e byte-code

Não vou me aprofundar nesse tema, que dá muita discussão, mas a idéia é só fazer uma diferenciação básica entre os 3 mundos.

Linguagens interpretadas em geral carregam todo o seu interpretador na memória, e conforme vão executando, traduzem as instruções de sua linguagem para instruções em linguagem de máquina. Um exemplo disso, é o velho VB (falo do VB6 ou ASP clássico, não do VB.NET). Quando você faz uma página ASP, simplesmente coloca ela no servidor e ela “funciona”. Já o VB clássico, é um pouco diferente. Como ele produz um “EXE” ele faz você pensar que é compilado, mas na verdade é o interpretador do VB, com teu código praticamente todo embutido como resource dentro dele. O antigo Clipper era assim também. Já o dBase (!) era um interpretador puro mesmo.

Linguagens compiladas, na sua etapa de compilação, traduzem o código fonte em linguagem de máquina. O binário (linguagem de máquina) não passa por toda a etapa de interpretação em tempo de execução, o que faz com que a execução seja muito mais rápida. O C, C++, Object Pascal (Delphi) são assim. Em geral, linguagens compiladas também são “não gerenciadas”, o que significa na prática que você deve controlar a gestão da sua memória por conta própria, o que traz muito controle e muito trabalho para o desenvolvedor.

A idéia do byte-code, veio para achar um meio-termo entre os dois mundos. O C#, VB.NET e Java são assim. Em tempo de compilação, você traduz da linguagem nativa (C#, Java) para uma linguagem intermediária. Assim são os assemblies .NET e os .class no java. Em tempo de execução, o framework traduz as instruções de byte-code para linguagem de máquina, porém, faz isso uma vez só. Esse é o processo do “JIT-Compiler” (just-in-time compiler). Em geral, a primeira execução é lenta pois passa pela compilação, mas nas etapas subsequentes, o tempo é mais ou menos parecido com uma linguagem compilada. Linguagens com byte-code em geral são “gerenciadas”, ou seja, possuem um garbage collector e cuidam de sua própria gestão de memória, o que por um lado diminui o trabalho do desenvolvedor, mas também diminui o controle do desenvolvedor sobre a gestão da memória (característica desejável para alguns tipos de aplicação).

Qual o problema com linguagens interpretadas?

O desempenho. Desconheço casos práticos em que linguagens interpretadas conseguem ser mais rápidas que compiladas e byte-code. O overhead de interpretar as instruções e traduzir em tempo de execução é muito grande.

Comparando entre as compiladas e byte-code, a discussão é mais complexa. O pessoal de compiladas (C++) costuma justificar que em casos extremos consegue fazer otimizações que os byte-codes não conseguem, e o pessoal de byte-code justifica que os ambientes “aprendem” conforme a execução e podem melhorar conforme o processo roda mais.

Eu percebo que na prática, JIT é mais lento que linguagem nativa no caso médio, porém, o custo vem no trabalho que dá fazer gestão de memória em linguagens compiladas. Acho que ambas tem suas aplicações.

Um micro-benchmark

Micro-benchmarks são péssimos e legais ao mesmo tempo. Péssimos porque não dá pra avaliar o desempenho de uma linguagem/ambiente num único SO, com um único exemplo bobo. Legais porque ajudam a ter uma idéia.

Resolvi fazer um exemplo muito simples. Um bubble sort (algoritmo popularmente conhecido por ser uma porcaria e gastar muitos recursos computacionais) e fazer o coitado ordenar uma matriz que está em ordem decrescente (pior caso). Assim ele gera O(n^2) comparações. Implementei o mesmo algoritmo em Python, C# e C++.

As implementações seguem:

C#


    public static class BubbleSort2
    {
        public static void Sort<T>(IList<T> list) where T: IComparable
        {
            bool swap = true;
            while (swap)
            {
                swap = false;
                for (int i = 1; i < list.Count; i++)
                {
                    if (list[i].CompareTo(list[i - 1]) < 0)
                    {
                        T tmp = list[i];
                        list[i] = list[i - 1];
                        list[i - 1] = tmp;
                        swap = true;
                    }
                }
            }
        }
    }

        [TestMethod]
        public void TestBigBubbleSort()
        {
            List<int> l = new List<int>(20000);
            for (int i = 20000; i >= 0; i--)
                l.Add(i);
            BubbleSort2.Sort(l);
        }

C++

#include <vector>

inline void Swap(std::vector<int> *l, int p1, int p2){
	int tmp = l->at(p1);
	l->at(p1) = l->at(p2);
	l->at(p2) = tmp;
}

void Sort(std::vector<int> *l){
	bool switched = true;
	while (switched){
		switched = false;
		for (int i = 1; i < l->size() - 1; i++){
			if (l->at(i) < l->at(i - 1)){
				Swap(l, i, i - 1);
				switched = true;
			}
		}
	}
}

namespace AlgorithmsTests
{		
	TEST_CLASS(UnitTest1)
	{
	public:
		
		TEST_METHOD(TestBigBubbleSort)
		{
			std::vector<int> l = std::vector<int>();			
			for (int i = 20000; i >= 0; i--)
				l.push_back(i);
			Sort(&l);

		}

	};
}

Python


def swap(list, p1, p2):
    tmp = list[p1]
    list[p1] = list[p2]
    list[p2] = tmp       

def bubble_sort(list):
    switched = True
    while switched :
        switched = False
        for i in range(len(list) - 1) :
            if i + 1 > len(list) - 1 :
                break
            if list[i] > list[i + 1] :
                swap(list, i, i + 1)
                switched = True
                
            
import unittest
from algorithms.core import bubble_sort

class BubbleSortTests(unittest.TestCase):
        
    def test_big_bubble_sort(self):
        l = [x for x in range(20000, 0, -1)]
        bubble_sort.bubble_sort(l)
    
if __name__ == '__main__':
    unittest.main()
       

Tempos de execução

Linguagem Tempo (ms)
C# 7.000
C++ 874
Python 109.199

Entenderam a razão da minha bronca com linguagens interpretadas?

Curiosidade inútil: Compilando em Debug, o C# demora 10s e o C++ em 1 minuto!

PyPy para o resgate

O PyPy é um JIT-Compiler para o Python. Neste ambiente, temos o seguinte resultado:

Linguagem Tempo (ms)
C# 7.000
C++ 874
Python (CPython) 109.199
Python (PyPy) 4.393

Como vemos no exemplo, apresenta um tempo de execução melhor que o C#. Interessante, né?

Resta saber se é um ambiente de execução maduro, mas é o suficiente para despertar meu interesse pela linguagem. Breve poderemos ter mais posts por aqui.

Por que números de ponto flutuante (Float e Double) são imprecisos?

Objetivo

Esses dias fui questionado por um colega de trabalho sobre usar números de ponto flutuante (float ou double) ou o tipo decimal para representar números.

Confesso que nunca tinha refletido a fundo nessa questão e aproveitei a oportunidade para pesquisar melhor o tema. Já tive muitos problemas com imprecisões em floats em casas decimais menos significativas, mas nunca fui pesquisar a razão.

A idéia desse post é compartilhar o aprendizado.

Representação interna

Como bem sabemos, todo tipo de dados tem uma representação interna, binária para o seu conteúdo. Essa representação é criada de uma forma a acomodar a maior quantidade de informações possíveis, no menor espaço. A mesma regra segue para os números de ponto flutuante.

Neste caso específico, optou-se por sacrificar um pouco a precisão em troca de espaço e velocidade de computação. Logo, a representação interna de um float (32 bits) é a seguinte (da esquerda para a direita):

  • 1 bit: sinal. 0 para positivo, 1 para negativo
  • 8 bits: expoente, representado em offset binary
  • 23 bits: mantissa

Esta notação segue um padrão IEEE 754.

Expoente

A notação do expoente, apesar de não ser lá tão simples, não é muito controversa. Ela segue a mesma regra de números inteiros (potências de 2), da direita pra esquerda. Ou seja, o primeiro bit da direita, representa 2^0, o segundo 2^1 até o oitavo bit 2^7.

A única diferença é que o “0” na verdade começa num offset de -127. Todos os bits zerados no expoente significa -127. Ao ligar o primeiro da direita, -126, o primeiro e o segundo, -124, de forma que quando ligamos todos os bits exceto o primeiro da esquerda (01111111), temos 2^0 = 1.

Mantissa

A mantissa é uma notação muito interessante. Quando todos os 23 bits estão desligados, não adicionamos nada ao número. Quando ligamos o primeiro bit mais significativo, ele adiciona metade do valor do expoente. O segundo bit, metade da metade do valor do expoente e assim sucessivamente.

Ex.: Se o expoente for 100000000 (decimal 2), o primeiro bit mais significativo do mantissa vale 1, o segundo vale 0,5, o terceiro 0,25 e assim sucessivamente.

Se o expoente for 011111111 (decimal 1), o primeiro bit mais significativo do mantissa vale 0,5, o segundo 0,25 e assim sucessivamente.

Se quisermos por exemplo o número 2.75, teremos expoente 100000000, mantissa (sem completar os digitos menos significativos) 011.

Outra curiosidade, se usarmos o expoente 100000000 e somente ligar o bit menos significativo do mantissa, seu valor é 0,0000002.

Tá bom, e as imprecisões?

Essa é a parte divertida. Por causa dessa característica da notação, alguns números não podem ser representados! Um bom exemplo é o número 0.1. Na verdade ele é composto por:

Sinal: 0
Expoente: 01111011 = 2^-4 = 0,0625
Mantissa: 10011001100110011001101 = 0,0375000014901161 (ver tabela abaixo)

Bit

Status

Valor
1

1

0,031250000000000000000000
2

0

0,015625000000000000000000
3

0

0,007812500000000000000000
4

1

0,003906250000000000000000
5

1

0,001953125000000000000000
6

0

0,000976562500000000000000
7

0

0,000488281250000000000000
8

1

0,000244140625000000000000
9

1

0,000122070312500000000000
10

0

0,000061035156250000000000
11

0

0,000030517578125000000000
12

1

0,000015258789062500000000
13

1

0,000007629394531250000000
14

0

0,000003814697265625000000
15

0

0,000001907348632812500000
16

1

0,000000953674316406250000
17

1

0,000000476837158203125000
18

0

0,000000238418579101562000
19

0

0,000000119209289550781000
20

1

0,000000059604644775390600
21

1

0,000000029802322387695300
22

0

0,000000014901161193847700
23

1

0,000000007450580596923830

Se fizermos a soma da tabela acima, chegamos no resultado 0,0625 + 0,0375000014901161 = 0,100000001490116.

A imprecisão 0,000000001490116 ocorre, porque na representação do número em bits não dá pra chegar exatamente nos 0.1.

Qual a implicação disso

Geralmente quando fazemos uma conta diretamente com float, não sentimos essa diferença, pois está numa casa decimal pouco significativa.

O grande porém está quando acumulamos os valores. Veja o snippet:

		static void Main(string[] args)
		{
			decimal d = 0m;

			for(int i = 0; i < 100000; i++)
				d += 0.1m;

			float f = 0F;
			for (int i = 0; i < 100000; i++)
				f += 0.1F;

			Console.WriteLine("Decimal: " + d.ToString()); //Decimal: 10000,0
			Console.WriteLine("Float: " + f.ToString()); //Float: 9998,557
		}

Era isso!

Referências

Utilizei como referência a Wikipedia e esta calculadora genial de números de ponto flutuante.

Também consultei o padrão IEEE 754 e a documentação da Microsoft.

Automatizando o versionamento no Build – Parte 2

Objetivo

Na parte 1, entendemos como o .NET faz para marcar a versão nos binários e como criar um arquivo para centralizar as propriedades comuns entre os assemblies.

Agora vamos brincar um pouco de como automatizar isso com o MSBuild. Se vc não sabe o que é o MSBuild, sugiro a leitura de MSBuild in a nutshell.

Automatizando o versionamento

A parte legal de utilizar uma ferramenta como o MSBuild é que é muito simples customizar a ferramenta e tem muita gente que contribui com tasks customizadas.

Nessa linha existe um projeto chamado MSBuild Community Tasks. Esse projeto possui além de uma centena de tasks, uma que iremos utilizar, a chamada AssemblyInfo.

Essa task nada mais faz do que gerar um arquivo com os atributos de versionamento. Ela tem a seguinte estrutura:

		<AssemblyInfo CodeLanguage="CS" OutputFile="$(MSBuildProjectDirectory)\..\src\SharedAssemblyInfo.cs"
			AssemblyProduct="MSBuildCodeMetrics - $(Branch) - $(CommitHash)" AssemblyCopyright="Copyright © 2013 Eric Lemes de Godoy Cintra" AssemblyTrademark=""
			AssemblyVersion="$(Version)" AssemblyConfiguration="$(Configuration)" AssemblyDescription="MSBuildCodeMetrics - $(Branch) - $(CommitHash)" />

Neste exemplo (usado no MSBuildCodeMetrics), gerarei um arquivo que sobrescreve o SharedAssemblyInfo.cs. No atributo AssemblyProduct, jogaremos além do nome do produto o valor das propriedades $(Branch) e $(CommitHash). A idéia é substituir estes valores neste e em outros atributos com valores gerados automaticamente pelo build.

Integrando com Git Hub

Seguindo a linha de contribuições, o MSBuild Community Tasks possui uma task que integra com o Git. A propriedade CommitHash pode ser obtida da seguinte maneira:

		<GitVersion>
			<Output TaskParameter="CommitHash" PropertyName="CommitHash" />			
		</GitVersion>

Esse trecho faz com que a task GitVersion pegue o valor do último commit hash e jogue na propriedade.

Aproveitando o conceito, mandei uma contribuição lá para o projeto, criando uma Task GitBranch. Essa task pegará o nome do branch do GitHub. Segue o exemplo:

		<GitBranch>
			<Output TaskParameter="Branch" PropertyName="Branch" />			
		</GitBranch>

Com essas combinações, podemos chegar no seguinte resultado final:

	<Target Name="assemblyinfo">
		<GitBranch>
			<Output TaskParameter="Branch" PropertyName="Branch" />			
		</GitBranch>
		<GitVersion>
			<Output TaskParameter="CommitHash" PropertyName="CommitHash" />			
		</GitVersion>
		<CreateProperty Value="0.0.0.0">
			<Output TaskParameter="Value" PropertyName="Version" Condition="$(Branch) == 'master'" />
		</CreateProperty>
		<CreateProperty Value="$(Branch)">
			<Output TaskParameter="Value" PropertyName="Version" Condition="($(Branch) != 'master')" />
		</CreateProperty>
	
		<AssemblyInfo CodeLanguage="CS" OutputFile="$(MSBuildProjectDirectory)\..\src\SharedAssemblyInfo.cs"
			AssemblyProduct="MSBuildCodeMetrics - $(Branch) - $(CommitHash)" AssemblyCopyright="Copyright © 2013 Eric Lemes de Godoy Cintra" AssemblyTrademark=""
			AssemblyVersion="$(Version)" AssemblyConfiguration="$(Configuration)" AssemblyDescription="MSBuildCodeMetrics - $(Branch) - $(CommitHash)" />
	</Target>

A idéia é simples. Pegamos o CommitHash e o Branch do GitHub. Se estamos no branch master, vamos carimbar a versão com “0.0.0.0” e no AssemblyDescription e ProductName ficaremos com algo parecido com: MSBuildCodeMetrics – master – b1e4cbf.

Se estivermos num branch ou numa tag, este número será utilizado para carimbar a versão. Por exemplo, para o meu branch 0.1, ficaremos com a versão da seguinte forma: MSBuildCodeMetrics – 0.1 – b1e4cbf. No AssemblyVersion, ficaremos com “0.1”, porém, infelizmente ao inspecionar pelo Windows, veremos a versão como 0.1.0.0. Ele sempre arredonda e coloca no formato de 4 dígitos. Eu particularmente não gosto, mas consigo ainda identificar minha versão pelo AssemblyDescription e ProductName.

Conclusão

Com um pouquinho de automação, como feito no MSBuildCodeMetrics, eu consigo com uma única execução da minha task “pack”, fazer um processo de automação do build de ponta a ponta. Eu gero o SharedAssemblyInfo.cs, realizo o build, monto a documentação e faço o pacote zip final, apertando o único botão. Acho que eu já consegui os três primeiros passos do The Joel Test.

Agora a cada vez que eu quero fazer um release de uma versão, basta criar uma nova tag e rodar o “pack” do meu build e está tudo pronto.

Segue código completo do script de build: MSBuildCodeMetrics.build

Automatizando o versionamento no Build – Parte 1

Objetivo

Conforme comentei no post anterior sobre a documentação do MSBuildCodeMetrics, uma outra coisa que eu queria fazer era automatizar o versionamento durante o Build.

O que eu quero dizer com isso é que eu queria “carimbar” meus binários com a localização em que eles estão no controle de versão.

A idéia deste post (ou série) é ilustrar como fazer isso na prática.

Adotando uma política de versionamento

O que eu chamo de política de versionamento é estruturar a organização de branches da aplicação, conforme explicado no post “Gestão de configuração e versões (SCM)”.

Apesar do projeto estar no GitHub adotei a política de branches de estabilização. Meu branch “master” possuirá a maior quantidade de features e será mais instável, e de tempos em tempos cortarei branches para “congelar” e estabilizar as features. No caso criei um branch 0.0 quando ainda não tinha nem documentação, e um branch 0.1 quando concluí o “básico” para soltar meu primeiro release.

A cada release que fizer, gerarei uma nova tag e o fonte compilado e publicado partirá desta tag. No meu caso, a versão 0.1.0 é a primeira tag e o primeiro release.

AssemblyInfo.cs

Muitos de vocês já devem ter visto o arquivo AssemblyInfo.cs. Geralmente ele fica jogado na pasta Properties e ninguém dá bola pra ele.

Ele tem mais ou menos essa estrutura:

using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

// General Information about an assembly is controlled through the following 
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("EricLemes.MSBuildTasks")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("EricLemes.MSBuildTasks")]
[assembly: AssemblyCopyright("Copyright ©  2013")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]

// Setting ComVisible to false makes the types in this assembly not visible 
// to COM components.  If you need to access a type in this assembly from 
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]

// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("dabe2f23-a2e2-4249-ad24-f597d0bd36eb")]

// Version information for an assembly consists of the following four values:
//
//      Major Version
//      Minor Version 
//      Build Number
//      Revision
//
// You can specify all the values or you can default the Build and Revision Numbers 
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

Na verdade essas propriedades no final das contas ficam em locais especiais nos binários e podem ser inspecionadas ao clicarmos com o botão direito num arquivo no Windows Explorer e depois em detalhes:

AssemblyInfo

Centralizando propriedades comuns a diversos assemblies

A primeira coisa interessante a fazer para automatizar a geração destes números é centralizar algumas das propriedades entre todos os assemblies. Pra mim essa idéia faz sentido porque sempre que lançamos uma nova versão do produto, queremos que todos os artefatos dessa versão sejam carimbados com a versão. Em outras palavras, nosso produto não é composto por um único binário e sim por um conjunto de binários.

Para tornarmos isso possível, primeiro devemos criar um novo arquivo em nossa solution. Eu achei interessante deixá-lo junto com os “Solution Items”. Clique com o botão direito, Add | New Item | Visual C# Class. No meu caso, chamei o arquivo de SharedAssemblyInfo.cs.

O arquivo contém a seguinte estrutura:

[assembly: System.Reflection.AssemblyProduct("MSBuildCodeMetrics - master - b1e4cbf")]
[assembly: System.Reflection.AssemblyCopyright("Copyright © 2013 Eric Lemes de Godoy Cintra")]
[assembly: System.Reflection.AssemblyVersion("0.0.0.0")]
[assembly: System.Reflection.AssemblyConfiguration("Release")]
[assembly: System.Reflection.AssemblyDescription("MSBuildCodeMetrics - master - b1e4cbf")]

Os atributos contidos neste arquivos são aqueles que devem ser iguais para todos os assemblies do projeto.

Em seguida, em cada um dos projetos (.csproj), clicamos com o botão direito, Add | Existing Item e escolhemos o SharedAssemblyInfo.cs. Invés de clicarmos em “Add”, clicamos na setinha de drop down e escolhemos “Add as Link”. Será adicionada somente uma referência do arquivo para o projeto. Por convenção a deixei dentro da pasta “Properties”.

Em seguida, cada um dos AssemblyInfo.cs contém a seguinte estrutura:

using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

[assembly: AssemblyTitle("MSBuildCodeMetrics.Core")]
[assembly: AssemblyCulture("")]
[assembly: Guid("4c6e2a2c-9e70-4b9f-a2fa-01bfdfdc577b")]
[assembly: ComVisible(false)]

Esses atributos, eu entendo que são de cada um dos assemblies.

Ao compilar o projeto agora, perceberemos que o Assembly possuirá todas as propriedades, mas apenas um arquivo precisa ser alterado para mexer no identificador de versão.

Por enquanto era isso.