Grid Computing – Parte 02

Neste tópico, vamos aprofundar um pouco na questão do paralelismo, nas formas de consegui-lo e os requisitos necessários para uma rotina ser quebrada em tarefas para aderir ao processamento em paralelo.

O paradigma do paralelismo

Como já vimos em posts anteriores sobre escalabilidade e performance, podemos ganhar ambos quebrando um grande processo ou tarefa em partes menores, e verificando se algumas destas partes podem ser executadas em paralelo. O interesse nessa área cresceu devido às limitações físicas que impedem o aumento da frequência (Clock) dos processadores. Porém, existe um grau de complexidade intrínseco no desenvolvimento de aplicações paralelas, em comparação com a codificação de um programa de processamento sequencial, pois sua implementação precisa prever e lidar com situações como a condição de corrida (race condition), onde a comunicação e a sincronização de diferentes sub-tarefas é uma das maiores barreiras para atingir desempenho em programas paralelos.

A abordagem científica e acadêmica destas dependências costuma utilizar um vocabulário complexo, vamos abordar de forma mais simples e direta. Uma determinada tarefa computacional complexa realizada por um algoritmo possui uma sequência chamada de ‘caminho crítico’. Este termo também é usado em Gerência de Projetos e Análise e Desenvolvimento de Sistemas. Na prática, nenhum pograma poderá rodar mais rápido do que a maior cadeia ou sequência de processos depententes. Entretanto, a maioria dos algoritmos não consiste de somente uma cadeia de processos dependentes, geralmente há oportunidades de executar cálculos independentes em paralelo.

Determinando tarefas paralelizáveis

As premissas gerais de etapas ou tarefas menores de processos que são paralelizáveis estão enumeradas abaixo, vamos abordar cada uma em mais detalhes ao decorrer do artigo. Vamos chamar de tarefa uma etapa do processo que desejamos avaliar sua aderência ao processamento paralelo.

1) Não pode haver dependências de valores

Os valores de entrada das tarefas não podem depender dos valores de entrada ou de saída de nenhuma outra tarefa. Por exemplo, um laço de execução que calcula um rendimento de um investimento em uma poupança não é paralelizável, pois o cálculo do rendimento de cada mês será executado com o saldo final do mês anterior.

2) Não pode haver condição de corrida

Nenhuma das tarefas deve alocar um recurso comum às demais tarefas em modo exclusivo. Isto caracteriza uma condição de corrida (race condition), e o tratamento disso implica na serialização da tarefa. Por exemplo, várias tarefas de processamento de notas fiscais de saída concorrentes, onde uma etapa de processamento envolve bloquear em modo exclusivo um registro do cadastro do produto para uma atualização de saldo ou um acumulador estatístico daquele produto vai eliminar o paralelismo de duas tarefas ou mais tarefas que forem ao mesmo tempo atualizar este produto, pois a primeira que conseguir o bloqueio exclusivo fará a atualização, enquanto as outras duas ficam esperando a liberação deste registro, jogando uma parte do ganho obtido com o paralelismo no lixo. Caso o acumulador em questão tiver que ser acessado por todas as tarefas, a coisa fica pior

Para este tipo de caso, deve ser verificado se compensa criar uma fila e/ou lista re requisições, e um processo único dedicado para estas requisições. Com apenas uma fila de requisições, esta pode tornar-se o caminho crítico deste processo. Tudo depende da velocidade que entram novos elementos nesta lista, e da velocidade que são realizados os processos dela, em relação às tarefas feitas em paralelo.

Por exemplo, se você têm 200 tarefas a realizar, onde cada uma demora em média 10 segundos, um processamento sequencial destas tarefas demoraria 2000 segundos ( 33 minutos aproximadamente). Porém, utilizando 10 processos simultâneos, cada processo executaria umas 20 requisições, onde o tempo e custo da infraestrutura de distribuição não será considerado, esse bloco de 20 requisições de 10 segundos cada demoraria 200 segundos, apresentando um ganho de 10 vezes, afinal cada um dos dez processos dedicados terminaria as suas atividades em 200 segundos, isso reduziria o tempo para 3 minutos e meio.

Porém, se destes 10 segundos, 1 segundo precisa ser dedicado ao acesso exclusivo de um recurso comum a todas as tarefas, cada acesso de uma tarefa colocaria as outras 9 em espera, a primeira tarefa em espera perderia 1 segundo, a segunda perderia 2, e assim sucessivamente, prejudicando bastante o tempo total para a finalização de todas as tarefas.

Para contornar isso, criamos uma fila e um processo dedicado a processar estas requisições, então cada tarefa demoraria 9 segundos, pois no momento que ela precisasse fazer o acesso exclusivo ao recurso, ela acrescentaria esta requisição na fila de um processo dedicado. Logo, cada um dos 10 processos dedicados ao processamento geral de tarefas executaria 20 requisições de 9 segundos, somando 180 segundos. No total de 200 requisições, cada uma colocaria em uma fila uma requisição de 1 segundo, logo o processo dedicado para processar a fila demoraria 200 segundos. Neste ponto, o processo da fila passou a ser o caminho critico, pois as tarefas distribuídas já terminaram, mas a fila ainda não está vazia. Mesmo assim, houve um ganho considerável em adotar esta medida.

Sabe-se neste ponto que, quanto mais processos em paralelo forem colocados para as tarefas de 9 segundos, , mais rápido estas tarefas serão executadas, mas a fila de tarefas sequenciais terá um tempo fixo de processamento, logo não será eficiente colocar mais de 10 processos dedicados, pois isto não vai diminuir o tempo da fila.

3) Considere o custo implícito na distribuição das tarefas

Quando o paralelismo das tarefas será executada em computadores distribuídos, deve-se levar em conta a latência na transferência dos parâmetros e do retorno das tarefas entre a aplicação que solicitou o processamento e o agente alocado para o processo. No exemplo anterior, desconsideramos o tempo de latência, mas sabe-se que o ganho escalar com o aumento de paralelismo não segue uma progressão geométrica.

Ao usar um mecanismo de distribuição de tarefas para múltiplos computadores, devemos levar em conta a latência de rede para trafegar os parâmetros de cada tarefa, isto dente a ser proporcional à quantidade de dados necessários, e pode acabar sendo um fator limitante do número de processos paralelos que você poderá usar simultaneamente, afinal não adianta deixar 50 processos dedicados se a banda de rede bater 100 % de uso com 35 processos, você começa a perder tempo para passar para cada processo o que ele tem que fazer.

Se o seu processo for muito curto, e a quantidade de dados trafegados for grande, o tempo gasto com a distribuição e controle destes processos pode consumir com a eficiência do paralelismo… neste caso estas tarefas poderiam ser executadas mais rápidas sequencialmente por um processo único, ou ainda poderiam ser distribuídas entre threads ou serviços na mesma máquina, usando recursos mais leves de comunicação entre processos na mesma máquina (como memória compartilhada por exemplo). Estas variantes serão abordadas em um próximo post.

Conclusão

No princípio isso pode parecer um monstro de duas cabeças, e quando você chega mais perto, parecem sete … Mas não entre em pânico, vamos nos aprofundar neste tema em doses homeopáticas, e depois de alguns exemplos de processos didáticos devidamente explicados, eu garanto que vai ficar mais fácil.

Até o próximo post, pessoal 😉

Referências

COMPUTAÇÃO PARALELA. In: WIKIPÉDIA, a enciclopédia livre. Flórida: Wikimedia Foundation, 2014. Disponível em: <http://pt.wikipedia.org/w/index.php?title=Computa%C3%A7%C3%A3o_paralela&oldid=38961464>. Acesso em: 30 nov. 2014.

CONDIÇÃO DE CORRIDA. In: WIKIPÉDIA, a enciclopédia livre. Flórida: Wikimedia Foundation, 2014. Disponível em: <http://pt.wikipedia.org/w/index.php?title=Condi%C3%A7%C3%A3o_de_corrida&oldid=40376481>. Acesso em: 30 nov. 2014.

CHEDE, CESAR TAURION. Grid Computing, um novo paradigma computacional. Rio de Janeiro: Brasport, 2004.

Grid Computing – Parte 01

Introdução

Hoje vou entrar em um assunto que não é novo, mas que é pouco explorado devido a complexidade intrínseca de implementação: Grid Computing. Lembrando dos princípios e técnicas de performance e escalabilidade, onde uma delas é procurar obter benefícios de escalabilidade com execução de operações simultaneamente em vários equipamentos (paralelismo).

A computação em grid ou distribuída pode ser vista como um tipo particular de computação distribuída, onde vários equipamentos completos e independentes (não apenas CPUs) são interconectados por uma rede (Ethernet), atuando juntos para processar grandes tarefas.

A definição de computação paralela e distribuída são muito próximas, mas é possível classificá-las independentemente por um critério básico: Em computação paralela, todos os processos têm acesso a uma memória compartilhada para trocar informações entre processos, enquanto na computação distribuída, os processos trocam mensagens entre si, normalmente por uma conexão de rede.

Tamanhos de Grid

Um Grid pode englobar computadores geograficamente próximos ou distantes, e consequentemente ter uma pequena, média ou grande escala. Um dos maiores projetos de Grid colaborativo em larga escala (mundial) foi o projeto SETI@home, da NASA, lançado em 1999, onde cada voluntário fazia o download de um programa para processamento de dados de áudio em seu computador, e quando o equipamento estava IDLE ou na proteção de tela, o programa baixava da internet automaticamente um pacote de áudio para análise, e após o processamento enviava o resultado de volta ao servidor. A capacidade de processamento deste conjunto, com aproximadamente 145 mil equipamentos ativos em 233 países era capaz de processar 668 teraFLOPS!

Onde cabe o uso de Grid

Normalmente Grids de computadores são usados na resolução de problemas matemáticos, científicos e acadêmicos, bem como em empresas para aplicações como descoberta de novos remédios, previsão monetária, análise sísmica, e processamento de retaguarda para comércio eletrônico e Web Services.

Porém, se um problema computacional pode ser adequadamente paralelizável, o uso de uma camada leve de infra-estrutura de grid pode permitir que programas stand-alone recebam partes do processo para serem executadas em várias instâncias de processamento distribuídas em múltiplos equipamentos.

Vale atenção especial na sentença “adequadamente paralelizável”, veremos que atender a esta premissa não é cabível a todos os processos de um sistema, e envolve mais de uma característica de processo, relacionadas a entrada e saída de dados e ao processamento em si. Veremos estes detalhes em profundidade no momento oportuno. Vamos ver primeiro onde ele pode ser aplicado!

Cálculo de Folha de Pagamento

Um cálculo da folha de pagamento dos funcionários de uma empresa, onde cada funcionário possui seu registro de ponto e histórico de exceções em um período. Este é um bom exemplo onde o paralelismo de um grid de processamento serve muito bem.

Em uma aplicação de cálculo de folha de instância única, um programa de cálculo de folha de pagamento é executado em um processo, onde este processo vai calcular, em sequência, a folha de pagamento de cada funcionário no período, um a um. Este programa, em uma única instância sendo executada em um único equipamento, não consegue utilizar todo o poder de processamento deste equipamento.

Logo, em um primeiro momento, podemos ganhar paralelismo alterando o programa para que ele iniciar vários jobs de processamento na mesma máquina, onde cada um deles recebe um código de funcionário diferente para calcular, onde o número de jobs iniciados indica quantos cálculos simultâneos serão feitos. O programa principal pega a lista de códigos ded funcionários a calcular, e passa o código de cada funcionário para o primeiro job livre, até que todos estejam ocupados, e cada job que termina de calcular um funcionário pega ou recebe o código do próximo da lista.

Agora sim, com um número maior de jobs, podemos usar todo o poder de processamento desta máquina, onde aumentamos o número de jobs dedicados até um ponto antes de algum limite computacional ser atingido, como tráfego de rede entre a máquina de processamento e o Banco de Dados, ou as CPUs do equipamento não atingirem 100%.

Se mesmo assim, o uso de uma máquina com jobs não seja suficiente, pois existe por exemplo um tempo curto para o fechamento da folha, e o número de funcionários cresceu com uma fusão de empresas ou absorção do quadro de funcionários de uma empresa do grupo, a solução é usar o processamento de mais de uma máquina. Neste caso, caberia bem o uso de uma infra-estrutura de Grid. Adicionando-se em cada equipamento uma ou mais instâncias de um serviço dedicado ao cálculo dos funcionários, e criando um mecanismo client-server entre o serviço que está solicitando o processamento dos funcionários e todos os jobs disponíveis e dedicados a este cálculo, vários funcionários são processados ao mesmo tempo em equipamentos diferentes, e os resultados de cada cálculo é gravado no Banco de Dados.

Ganho esperado

Esta escalabilidade não é infinita, mais cedo ou mais tarde algum limite computacional do conjunto vai ser atingido, e não necessariamente dobrar o número de máquinas vai dobrar a sua capacidade de processamento, afinal temos que levar em conta uma fatia de tempo do mecanismo de distribuição em trafegar os códigos para o cálculo, e que cada processo pode pedir mais informações ao mesmo tempo e de funcionários diferentes para o Banco de Dados, mas mesmo assim um processo que demorava 10 horas para ser executado em 4 jobs dentro de uma máquina, ao ser divididos em 12 jobs distribuídos em 3 máquinas pode terminar a tarefa completa por exemplo em apenas 4 horas.

Dificuldades intrínsecas

Uma das primeiras dificuldades em um Grid deste tipo é determinar o número ideal jobs dedicados por máquina a serem dedicados para um processamento distribuído. Este número é empirico, e será determinado pela natureza do processamento versus capacidade de tráfego de dados e transações do ambiente computacional. Normalmente um grid busca aproveitar o poder de processamento de um conjunto de máquinas que normalmente estão executando outros processos de outras partes da aplicação, que não consomem todo o recurso computacional da máquina. Porém, um número fixo de jobs de grid executados nesta máquina, junto com outros processos em execução, podem atingir um limite computacional deste equipamento, prejudicando os outros processos desta máquina. A engine de Grid ideal precisaria ser capaz de monitorar as máquinas envolvida, atuando sobre a quantidade de jobs dedicados durante a execução, para atingir a “crista” da onda da curva de desempenho sem esgotar os recursos das máquinas envolvidas.

Determinar quais processos aderem com eficiência à execução em Grid, como eu mencionei anteriormente, é um processo um pouco mais complexo, e envolvem muitas características do processamento, e devido à sua extensão, este tema será abordado com uma lupa em um próximo post.

Deve-se também projetar uma infra-estrutura para distribuição das mensagens de processamento de forma eficiente e o mais leve possível, garantindo ao final do processo que todas as mensagens tenham sido entregues e processadas com sucesso, e em caso de falha ou ausência de retorno — como por exemplo uma queda ou término anormal de um agente de processamento — esta infra-estrutura possa permitir por exemplo o re-envio daquela requisição a outro agente, ou notificar o programa cliente desta ocorrência para que este tome a decisão sobre o que fazer.

ERP Microsiga e Grid de Processamento ADVPL

O ERP Microsiga Protheus possui algumas rotinas que utilizam um mecanismo de processamento em Grid, onde cada máquina virtual servidora do Protheus pode ser configurada como um agente de processamento, e um serviço único é configurado como o coordenador da distribuição dos agentes para processos do Grid ADVPL. Cada funcionalidade que utiliza o Grid compartilha internamente de uma camada comum de abstração “client” do Grid, onde o programador deve implementar uma função de preparação de processo dedicado e uma função de execução de requisição, e a infra-estrutura do grid garante a inicialização dos processos, distribuição de requisições e controle de execução das requisições de um processo que se utiliza do Grid. Não conheço todas as rotinas, mas sei com certeza que o cálculo de folha de pagamento do ERP possui uma opção de processamento para usar o Grid ADVPL.

Este post é a pontinha do iceberg … a cada post deste tema vamos nos aprofundar um pouco mais nas particularidades deste paradigma, que pessoalmente eu acho muito, muito interessante ! Até o próximo post, pessoal 😉

Referências

Wikipedia contributors. Distributed computing. Wikipedia, The Free Encyclopedia. November 20, 2014, 04:59
UTC. Available at: http://en.wikipedia.org/w/index.php?title=Distributed_computing&oldid=634650954.
Accessed November 27, 2014.

Wikipedia contributors. Grid computing. Wikipedia, The Free Encyclopedia. November 23, 2014, 13:50 UTC.
Available at: http://en.wikipedia.org/w/index.php?title=Grid_computing&oldid=635101070. Accessed November
27, 2014.

Wikipedia contributors. SETI@home. Wikipedia, The Free Encyclopedia. November 26, 2014, 13:42 UTC.
Available at: http://en.wikipedia.org/w/index.php?title=SETI@home&oldid=635510528. Accessed November 27,
2014.

Boas Práticas – Identação e Nomenclaturas

Seguindo a linha das boas práticas, adotar um padrão de codificação e organização de código ajuda muito a compreensão do código e de sua funcionalidade. Estas técnicas existem em todas as linguagens de mercado, e independente do padrão adotado, é uma boa prática ter um padrão. Vou enumerar neste post apenas duas práticas importantes, que embora sejam aplicáveis à maioria das linguagens de mercado, serão vistas com enfoque na linguagem ADVPL: Identação e nomenclatura de variáveis e constantes.

Tornar o código mais facilmente legível usando padrões e boas práticas de desenvolvimento aumenta a legibilidade do código e facilita a compreensão do mesmo pelos outros integrantes da equipe, e vai permitir que você saia de férias sem que seu telefone pareça a campainha de intervalo de aulas da escola (tocando a cada 45 minutos).

Identação

Cada um têm seu gosto, e gosto não se discute. Mas quando se trabalha em equipe, manter um padrão de identação único e configurado e aplicado da mesma forma em todo o código, deixa o código mais bonito e legível. Tanto faz se a identação será tabulada ou com espaços, desde que seja igual para todos. Existem editores de código configuráveis, inclusive com opção de refatoração apenas da identação, com opções pré-configuradas para múltiplas linguagens. Normalmente eu utilizo identação com dois espaços em branco, para legibilidade de código em múltiplas plataformas ou editores com menos recursos gráficos. Pra mim, este tamanho é mais que suficiente para ser perceptível sem afastar demais os códigos horizontalmente.

Porém, se eu vou dar manutenção em um projeto onde todos os fontes foram identados usando outro padrão, eu confirmo se este é o padrão a ser mantido, e ao dar manutenção eu respeito o padrão já existente. Não existe o padrão certo ou o errado, existe o que melhor atende a necessidade da equipe que o utiliza. O grande pecado na minha opinião é não identar o código.

Nomenclatura de variáveis e constantes

Embora o ADVPL permita que cada variável seja declarada sem um tipo específico, e durante a execução do código uma variável possa receber qualquer tipo de conteúdo por atribuição, normalmente as funções e rotinas criadas recebem parâmetros normalmente de um mesmo tipo, e dentro das funções as variáveis locais costumam também receber um tipo único de valor durante sua execução, logo é uma boa pratica prefixar as variáveis do código com pelo menos uma letra minúscula indicando o seu tipo, seguida por um nome legível relacionado diretamente com o seu conteúdo, usando letras maiúsculas na primeira letra do nome da variável, e nas demais letras que iniciem um nome composto, respeitando o limite de caracteres da linguagem em questão.

Um complemento ao prefixo pode ser acrescentado, no caso de funções que utilizam variáveis de escopos diferentes, para identificar o escopo da variável. No Advpl por exemplo, podemos ter variáveis de escopo LOCAL, PRIVATE, STATIC e PUBLIC. Todos os parâmetros declarados de uma função por padrão são LOCAL, e dentro do corpo da função podemos declarar variáveis LOCAL, PRIVATE e PUBLIC, e ainda fora do corpo da função, mas dentro do mesmo arquivo-fonte, podemos declarar variáveis STATIC, e acessá-las dentro de qualquer função daquele fonte. Assumindo que todas as variáveis locais sejam prefixadas apenas com o tipo da informação que ela deve conter, pode-se prefixar as variáveis STATIC com o prefixo “s”, PRIVATE com prefixo “p” e PUBLIC com “g”, seguidas da letra que identifica o tipo da variável, seguida pelo nome efetivo da varíavel, utilizando as letras significativas do nome em maiúsculo. Eu já ví fontes onde até mesmo as variáveis declaradas explicitamente dentro de uma função recebem um prefixo “l” (L minúsculo), para ser fácil diferenciar de uma variável que foi recebida como parâmetro, que também têm escopo local. Já para constantes ou #defines, podemos por exemplo prefixar com o tipo seguido do nome da constante, e para esta nomenclatura ser diferente das variáveis locais, colocamos todas as letras maiúsculas.

Por exemplo, na linguagem ADVPL podemos ter os tipos A ( Array ) , C ( Caractere ) , D ( Data ) , N ( Numérico de ponto flutuante ), F ( Numérico com precisão Fixa ), L ( Lógico ) , B ( Bloco de Código ) e O ( Objeto ).  Seguindo uma determinação purista, se dentro do corpo de uma função você encontra as variáveis abaixo, rapidamente é possível concluir seu escopo e tipo de dado esperado, e ter uma idéia de seu conteúdo:

lnValTemp // LOCAL, numérica
lnSubtot // LOCAL, numérica
aItensVend // LOCAL (parametro), Array
nFatorMult // LOCAL (parametro), numérico
slBlind // STATIC, lógica
pcCpoTit // PRIVATE, caractere
gcMsgExit // PUBLIC, caractere

Note que as duas primeiras letras identificam o escopo e o tipo da variável, isto fica claro, mas nem sempre o nome vai lhe revelar logo de cara tudo o que você precisa saber sobre a variável, mas vai lhe dar uma boa ideia. Basta dar uma olhada na documentação no cabeçalho da função (outra boa prática, que veremos em mais detalhes em um outro post) ou dentro do corpo da função, para ver onde e como a variável é utilizada. O nome já lhe dá uma ideia, ver o fonte apenas esclarece a funcionalidade.

E, novamente, independentemente do padrão adotado, é elegante que ele seja único. Caso um padrão seja adotado ou alterado, isto deve ser uma decisão acertada e alinhada com a equipe, para que todos remarem na mesma direção. E, se você já têm uma porção de código já escrita, não precisa sair alterando e refatorando tudo, isto deve ser feito com calma para não gerar mais retrabalho ou efeitos colaterais, e pode demandar um tempo que você não têm na cartola. Isto pode muito bem ser feito sob demanda, quando o código receber manutenções de correção ou melhoria.

E, por hoje é só, pessoal !! Até o próximo post 🙂

A importância da simplicidade

Boas práticas existem na execução de qualquer atividade, desde colocar um copo no armário da cozinha, escovar os dentes, ou desenvolver uma rotina em um programa. Muitas dessas boas práticas são ensinadas e frisadas nas abordagens didáticas do planejamento, desenvolvimento, implementação e suporte de sistemas informatizados, algumas inclusive são tão importantes que deveriam ser vistas como “regras de ouro”. No que diz respeito a programação, existem boas práticas associadas ao desenvolvimento do algoritmo independente da linguagem, e outras de aplicação específica relacionadas à ferramenta ou linguagem utilizada.

Ao iniciar este texto, a ideia original era abordar uma lista de boas práticas aplicáveis a qualquer linguagem, então comecei pela que eu considero uma das mais importantes: A Simplicidade. Porém, durante a elaboração do texto, foram necessários mais parágrafos para desenvolver o assunto. A frase de Martin Fowler em um de seus livros sobre refatoração de código aborda de forma simples e direta este assunto: “Qualquer um pode escrever um código que o computador entenda. Bons programadores escrevem códigos que os humanos entendam.”

Escrever um código complexo não necessariamente significa que ele foi escrito por um programador com profundo conhecimento da linguagem ou mesmo que ele sabia o que estava fazendo. Ou o código já nasce complexo, normalmente quando não se usa a melhor abordagem para o algoritmo e insiste-se numa linha de raciocínio que vai tornar possível resolver a questão, mas não é a melhor escolha, ou o código inicial nasce simples mas torna-se complexo ao ganhar mais funcionalidades, normalmente não previstas na análise inicial de sua concepção, ou que exigiriam uma refatoração cujo custo não poderia ser absorvido naquele momento, então mantém-se a lógica inicial e a nova funcionalidade ou comportamento fica parecendo um “remendo” no fonte.

A simplicidade é amiga do próprio programador. Afinal, depois de uma semana, um mês ou um ano, se o próprio desenvolvedor não registrou alguns “por quês” no código, nem ele vai lembrar por que optou por fazer daquela forma, e vai ter que reestudar o próprio código para entrender o que ele faz. Um programador experiente consegue fazê-lo, porém isso torna-se cada vez mais difícil e custoso quanto mais o código cresce desordenadamente, e até virar um mostro marinho de cinco pernas, três braços, tromba e chifres, e ninguém mais quer mexer naquilo, com medo de um dos braços do monstrinho possa lhe enfiar a mão na sua nuca e te arrancar a espinha! E isto terá sérias consequências no plano de carreira desse programador ao assumir novas responsabilidades ou projetos, pois não é possível ele subir um degrau na carreira se não houver alguém capacitado para preencher o degrau que hoje ele ocupa, e que ficará vazio com a sua subida.

Existem casos onde o código têm um nível intrínseco de complexidade, não por estar mal escrito ou remendado, mas onde a natureza do processo é complexa, ou ele foi escrito de uma forma mais complexa para obter ganhos diretos ou agregados, como um diferencial crucial de performance ou escalabilidade para um fim específico, onde este ganho justifica a complexidade da sua implementação e seu custo de manutenção. E mesmo assim, um bom programador neste ponto, usando sabiamente de um ou mais comentários sobre a lógica e os fatores determinantes da escrita da rotina, vai tornar o entendimento do código mais simples.

Procure sempre a simplicidade, grandes problemas da humanidade são resolvidos de forma simples. Utilize o complexo apenas quando realmente necessário, quando realmente existe um ganho tangível, pois a complexidade naturalmente vêm com um custo agregado, que nem sempre é necessário.

Até o próximo post 🙂

Referências

A melhor linguagem de programação

Enfim, vou abordar um tabu, tema de altas rixas e discussões acaloradas entre os defensores de linguagens procedurais, orientadas a objeto e eventos, proprietárias ou de uso livre, interpretadas e compiladas, que normalmente surgem nas rodas de analistas e programadores na hora do café, num fórum, chat, blog, …

Normalmente, um especialista em uma determinada linguagem XYZ usa e sabe usar até o osso dos recursos da linguagem na qual ele se especializou, e este cidadão trabalha no desenvolvimento de soluções para um segmento de mercado ou com uma linha de aplicativos onde esta linguagem é uma boa escolha. Logo, para este especialista, a linguagem XYZ é a melhor linguagem.

Outro especialista, que trabalha em um segmento similar, mas com foco em outros tipos de soluções, e que em um determinado ponto resolveu se especializar em programar na linguagem ABC, que se comparada com XYZ possui alguns pontos de vantagem e outros de desvantagem, para este especialista em ABC, esta para ele é a melhor linguagem, pois ele consegue fazer em ABC tudo o que outro desenvolvedor faria em XYZ.

Já um terceiro especialista, que trabalha em um segmento de programação mais específico para sistemas embarcados em um tipo de hardware, especialista em GHI, este vai justificar a sua escolha de forma similar, pois na prática, a melhor linguagem de programação é aquela na qual você se especializou, em um nível que você consegue utilizá-la para atender às necessidades do desenvolvimento da aplicação ou solução em tecnologia de informação que o seu cliente precisa. Sem maiores rodeios ou delongas.

Faz mais de um ano, meu amigo Pedro Reis Lima, que inclusive devo uma visita, me pediu uma recomendação de uma linguagem de programação, pois ele se interessou pelo tema, frisando o fato dele não ter conhecimentos sobre programação, e pensando na utilidade e aplicabilidade deste conhecimento. Ele me perguntou sobre C++ ou Java, o que eu lhe recomendaria, ou mesmo uma outra linguagem. Durante um ou dois dias, eu lhe respondi o que eu achava pelo Messenger do Facebook, e após uma leve re-editoração, acho que a resposta que eu lhe dei cabe exatamente neste contexto.

Cada linguagem de programação têm um nicho de mercado onde ela é mais utilizada, existem linguagens que oferecem mais atrativos e facilidades para desenvolver tipos de aplicações ou sistemas, … um desenvolvedor em C ou C++ escreve qualquer coisa, usando as funções padrão da linguagem, ou usando outros componentes de alto nível … Se você pretende desenvolver aplicações para Android, existe o Android SDK, para Windows têm o Visual Studio .NET, que permite você desenvolver usando várias linguagens, C#, Objective C, Visual Basic, etc… Se você vai trabalhar com um ERP de mercado, ou desenvolver customizações neste ambiente, você precisa conhecer a linguagem que ele foi escrito … o ERP da Microsiga usa ADVPL, o ERP da Datasul usa ABL, RM Sistemas usa .NET ( Visual Studio ), o SAP usa ABAP … Se você vai desenvolver aplicações para WEB, você pode usar PHP, ASPNET, Python, Ruby … Para armazenamento e recuperação de dados, é fundamental conhecer SQL, o universo de possibilidades é imenso.

Se eu fosse recomeçar a aprender informática hoje, ou recomendar um caminho a seguir, eu lhe diria inicialmente para estudar lógica de programação — apender por exemplo uma meta-linguagem, como o Visualg … é uma linguagem didática, parece um “portugol” — pois isto será algo comum a praticamente todas as linguagens. Num segundo passo, estudaria as diferenças entre uma linguagem procedural, uma estruturada, e uma orientação a objetos, e dedicaria atenção especial a esta última. Se você tem em mente trabalhar com alguma linguagem comercialmente viável, dê uma olhada no que existe de demanda de mercado… Se hoje eu não conhecesse C++ ou Java, eu também ficaria em dúvida, e provavelmente eu escolheria Java.

Mas lembre-se que você não vai se tornar especialista da noite para o dia, isso é um investimento de médio a longo prazo, e de muita dedicação. Justamente por isso, vou lhe adiantar uma coisa: você precisa gostar de mexer com isso, e se entregar a essa atividade com gosto, fazer isso apenas pelo retorno financeiro vai tornar seu trabalho inicialmente cansativo, até tornar-se insuportável.

Ao tornar-se especialista em uma linguagem, você conseguirá desenvolver as soluções para diversas necessidades dos seus clientes, mas não se prenda em aprender apenas uma linguagem. Seja especialista em uma, mas saiba e conheça pelo menos três ou quatro linguagens, pois podem existir problemas ou necessidades dos seus clientes ou restrições e fatores em um projeto onde uma outra linguagem poderá ser mais eficiente ou mais aderente à solução que você precisa projetar.

Com o crescimento da demanda de desenvolvimento de aplicações e soluções, algumas previsões apontam que a quantidade de profissionais disponíveis no mercado em alguns anos não vai atender a demanda… Não posso lhe dizer qual é a melhor… Hoje, e pra mim, C++ e ADVPl resolvem todos os meus problemas, mas volta e  meia eu uso Python e ainda dou manutenção em fontes em Pascal (Delphi).

Aprenda uma, e aprenda bem, depois de você aprender bem a primeira linguagem, aprender uma segunda ou terceira linguagem vai ser bem mais fácil, eu garanto. Separei alguns links de sites e reportagens para dar um pouco mais de luz neste tema, sites, blogs, reportagens, acredito que estes links serão úteis. Pesquise mais sobre este tema, para quem descobre logo no início que têm uma afinidade com este tipo de trabalho, é um prato cheio pra seguir uma carreira.

http://www.devmedia.com.br/e-agora-qual-linguagem-de-programacao-escolher/15444
http://tisimples.wordpress.com/2012/10/30/como-escolher-a-melhor-linguagem-de-programacao-para-seu-projeto/
http://webinsider.com.br/2009/09/16/linguagens-sao-linguagens-mas-qual-devo-escolher/
http://www.ebc.com.br/noticias/economia/2013/07/profissionais-de-tecnologia-da-informacao-tem-maior-chance-de-emprego
http://g1.globo.com/jornal-hoje/noticia/2013/06/setor-da-tecnologia-da-informacao-oferece-276-mil-vagas-em-todo-o-pais.html

Este útimo site em especial, faz parte de uma iniciativa da Softex, em parceria com o governo federal e iniciativa privada para oferecer cursos de informática gratuitos, na modalidade de ensino a distância, são mais de 30 cursos e mais de 1500 horas de capacitação.

http://www.brasilmaisti.com.br/index.php/pt/explore/projeto

Saudações, e até o próximo post 🙂

Desmistificando a programação

Para você, que está iniciando ou se interessa em programação, mas acha que isso é coisa de outro mundo, eu tenho uma notícia muito boa: Pode parecer difícil, mas na verdade não é. A complexidade de um sistema começa a surgir na integração de suas partes, não exatamente em programá-lo. Este tópico é muito interessante para quem está começando agora a interessar-se por programação, e vai lhes dar uma noção do que é um programa de computador, e inclusive vai lhe dar as instruções parar fazer um programinha bem simples, que pode ser executado em qualquer navegador de internet ( Web Browser ) instalado no seu computador.

Cada linguagem de programação é apenas um dialeto de instruções, que possui uma gramática e sintaxe próprias, como se fosse um outro idioma. No final das contas, visto de forma muito simplista, um programa de computador não passa de uma lista de instruções para ler, transformar ( ou processar ) informações, composta de instruções e recursos fornecidos por uma linguagem ou ambiente, para realizar uma tarefa.

Há quase 20 anos atrás, tive a oportunidade de ministrar aulas de Informática, foi um capítulo a parte na minha vida de programador, muito gratificante ao ver como os alunos admiraram a simplicidade da abordagem dos conceitos de programação e a satisfação minha e deles de entender como um programa de computador funciona.

Um ex-professor e amigo meu, Sr. Regginato, foi um dos professores que me introduziu no mundo do IBM-PC e dos conceitos da informática e do processamento de dados, em uma escola que ele montou junto com sua esposa em Botucatu, interior de São Paulo. Acho que foi dele a frase “Programar um computador é como escovar os dentes.”

Parece loucura, mas é exatamente isso. Imagine que você precise escrever uma lista de passos para ensinar uma pessoa a escovar os dentes. como você faria ? Naturalmente, você deverá criar uma espécie de manual com os passos para realizar esta tarefa, com um mínimo de detalhes que tornem o processo intuitivo, e naturalmente escrito em um idioma que a pessoa conheça. Por exemplo:

Para escovar os dentes:

1) Vá até o banheiro, abra o espelho e localize a sua escova de dentes.
2) Verifique se a escova de dentes está no espelho.
3) Caso não esteja, verifique se ficou sobre a pia.
4) Verifique se têm pasta de dentes no banheiro, e se têm água na pia.
5) Caso algum dos itens acima não esteja pronto para uso, não é possível escovar os dentes.
6) Pegue a escova de dentes, abra a torneira e molhe um pouco a cabeça da escova.
7) Pegue a pasta de dentes, abra-a e posicione a saída do tubo na cabeça da escova.
8) Pressione um pouco o final do tubo até que saia pasta suficiente para colocar sobre as cerdas da escova.
9) Feche a pasta de dentes e guarde-a no espelho.
10) Insira a escova na boca, e espalhe a pasta na sua arcada dentária inferior.
11) Esfregue a escova entre a arcada superior e inferior, cerrando os dentes para facilitar a operação.
12) Esfregue lateralmente as arcadas superior esquerda e direita, inferior esquerda e direita e a parte frontal superior e inferior.
13) Repita o passo 12 por duas vezes.
14) Durante o processo, respire pelo nariz para não engolir pasta de dentes ou a espuma deixada pelo processo de escovação.
15) Cuspa o excesso de pasta e espuma na pia.
16) Abra a torneira, pegando a água com uma das mãos, e leve à boca para enxaguar.
17) Bocheche a água para limpar a pasta e cuspa a água com pasta na pia.
18) Volte ao passo 16 e repita-o por duas vezes.
19) Lave a escova de dentes, tirando as sobras de pasta e espuma.
20) Feche a torneira, e guarde a pasta no espelho.
21) Pronto, seus dentes estão escovados.
22) Repita a operação de escovação sempre após as refeições.

Para nós é uma coisa simples, mas descrever a lista de tarefas e passos para escovar os dentes incluem muitos detalhes que nós não prestamos mais a atenção, pois aprendemos isso desde pequenos e sabemos de cor e salteado. A lista de tarefas acima foi escrita em uma linguagem que nós chamamos de “Linguagem Natural”. Programar um computador basicamente é criar uma lista de instruções, a qual chamamos de “programa”, escrito em uma linguagem ou dialeto que a linguagem entende, para que o computador execute a lista de instruções quando você solicitar a ele a “execução” do programa.

Vamos ver agora um exemplo mais simples: Como fazer uma operação de soma de números inteiros em uma folha de papel:

1) Usamos uma folha de um caderno, e escrevemos os dois ou mais números a serem somados alinhados à direita
2) Após escrever os números, passe um traço sob o último número. Abaixo deste traço será escrito o resultado da soma.
3) considere a coluna mais à direita na vertical, e some todos os números dessa coluna.
4) Caso a soma dos números resulte em um número menor ou igual a nove, escreva este número na ultima coluna em baixo do traço.
5) Caso o número seja maior do que nove, escreva na última coluna da direita abaixo do traço o último digito à direita do resultado calculado, e copie o dígito da esquerda do resultado na coluna imediatamente anterior, na linha em branco antes do número.
6) Repita a operação a partir da etapa 03, considerando agora a coluna anterior (à esquerda) da ultima coluna calculada, levando em conta que caso tenha sido escrito um número na linha superior vindo da soma da ultima coluna, este número deve ser considerado na operação.
7) Ao realizar a operação até a primeira coluna, caso algum digito adicional tenha sido colocado na coluna anterior, ele deve ser copiado para baixo, no resultado.

Agora vamos executar este procedimento, usando os números 12444, 312 e 9644.

  12444
+   312
+  9644
 -------

No nosso exemplo, 4 + 2 + 4 = 10, logo copiamos o zero na ultima coluna da linha do resultado, e o numero 1 na coluna anterior, na linha imediatamente superior aos números a serem somados.

    1
 12444
+  312
+ 9644
-------
     0

Agora, voltamos na etapa 3 e somamos 1+4+1+4 ( 10 novamente), copia o zero no resultado e joga o um na coluna da esquerda na linha superior

   11
 12444
+  312
+ 9644
-------
    00

Agora somamos a terceira coluna, 1+4+3+6 ( 14 ), copia o 4 pra baixo e o um na coluna da esquerda na linha superior.

  111
 12444
+  312
+ 9644
-------
   400

Ao somar a quarta coluna, ignoramos os espaços em branco das colunas que não tem número, e somamos 1+2+9 ( 12 ). Copia o dois pra baixo, e o um na coluna da esquerda na linha superior.

 1111
 12444
+  312
+ 9644
-------
  2400

E agora, por ultimo, somamos a coluna anterior ( nossa última coluna com números), 1+1 (2) e copiamos o resultado para baixo.

 1111
 12444
+  312
+ 9644
-------
 22400

E chegamos ao resultado 22400. Pode fazer na calculadora, vai dar o mesmo resultado …rs…

Agora, vamos fazer um programa de computador para somar três números. Vou escolher uma linguagem script, que qualquer Browse de internet possa executar. Usando uma máquina com qualquer versão do Windows, abra o bloco de notas do seu computador, e crie um novo arquivo de texto. Chame-o de “exemplo.html”, e salve-o na sua pasta de documentos.

Agora, cole o conteúdo abaixo dentro deste arquivo:

// <![CDATA[
a = 12444;
b = 312
c = 9644;
d = a+b+c;
alert("Resultado = "+d);
// ]]>

Salve o arquivo, ele tem que ter a extensão HTML, então execute o arquivo clicando duas vezes em cima do nome do arquivo na sua pasta de documentos. O Windows vai verificar qual é o programa usado como padrão para navegação WEB, capaz de abrir e mostrar páginas HTML, e chamá-lo para mostrar o conteúdo do arquivo. Como o arquivo contém um script, o navegador sabe o que o script singifica e vai executá-lo. Seu resultado em tela deve ser uma mensagem parecida com a imagem abaixo:

Featured image

Traduzindo o script acima: utilizamos algumas letras do alfabeto para receber valores. A recebe o valor 12444, B recebe o valor 312 e C recebe 9644, e D recebe o resultado da soma dos conteúdos de A com B com C, e por último chamamos uma função da linguagem JavaScript para mostrar o resultado em uma janela na tela do Browser.

Tão simples quanto isso 🙂

Se você chegou até aqui e fez o exemplo acima, meus parabéns, você fez um pequeno programa de computador! Não foi necessário refazer toda a sequência de operações que teríamos de fazer ao somar um numero no papel, afinal a linguagem utilizada oferece operadores aritméticos prontos com estas funcionalidades. Para executar este programa, internamente o browser chamou um interpretador, que traduziu o programa para chamar instruções específicas do processador da sua máquina, para ler os conteúdos dos números e somá-los.

A partir de um exemplo simples, também conhecido como ‘Hello world’, iniciamos a construção de programas mais complexos, utilizando mais funções da linguagem escolhida, funções que permitem perguntar valores ao usuário da maquina, ou obter valores em um formulário, desenhar uma janela com botões e escrever as ações que cada botão deve executar, ler e gravar informações em um arquivo no disco ou em um Banco de Dados, etc. A maioria das linguagens de desenvolvimento de mercado permitem estes tipos de operação, cada uma do seu jeito particular, a complexidade do programa tende a crescer de acordo com a quantidade de dados e complexidade do processo a ser realizado com estes dados.

Até o próximo post !!

Escalabilidade e performance – Técnicas

No post anterior, sobre escalabilidade e performance, entramos na definição de ambas, e relacionamos os princípios comuns que visam a aplicação atingir este objetivo. Apenas para refrescar a memória, os princípios são:

  1. Aproximar o algoritmo dos dados
  2. Aumentar o paralelismo
  3. Não estabelecer afinidade
  4. Minimizar contenções
  5. Minimizar o uso de recursos
  6. Pré-alocar e compartilhar recursos caros

Agora, vamos abordar as técnicas normalmente utilizadas nas aplicações atualmente, que fazem uso destes princípios.

Minimizar Interações entre fronteiras

Entre um algoritmo/programa e o SGDB (Banco de Dados) normalmente temos uma interface de rede, nem sempre ambos estarão na mesma máquina. Vamos chamar a interface de rede ou um gateway de acesso de “fronteira”.

Para conseguir aumentar o paralelismo de processos, podemos utilizar múltiplas CPUs diferentes (princípio 2), porém para isso ferimos o princípio 1 (Algoritmo mais perto dos dados). Neste caso, como queremos os processos separados e paralelos, aceitamos o preço de ter uma fronteira em prol do paralelismo, e como sabemos que o uso da fronteira têm um custo, a solução é minimizar as interações, seja diminuindo o número de chamadas, e/ou diminuindo o volume de dados passados para apenas o necessário

Obtenha o mais tarde e libere o mais cedo possível

Esta é uma recomendação bem entendida, mas muito pouco obedecida pelos desenvolvedores. São os Princípios 4 e 5 que pedem que esta regra seja obedecida, mas na maior parte dos casos, o modelo das classes ou do algoritmo não leva em conta a performance. É comum fazer um modelo de classes ou funções, determinar seus métodos, e perder o contexto do uso dos recursos que são utilizados por estes métodos. Logo estamos fazendo o diagrama de interação e não sabemos mais quando começaram as operações que fazem bloqueio de registros no banco de dados ou que simplesmente alocam conexões de um pool. Em consequência, aumentamos o tempo de contenção desnecessariamente, deteriorando o desempenho do sistema em momentos de stress.

Um esboço para um padrão de codificação que busca uma performance melhor deve incluir os seguintes passos:

  • Coletar todos os dados possíveis que não precisem do Banco de Dados (parâmetros, cache, etc);
  • Coletar todos os dados do SGDB (com o mínimo de interações) que não impliquem em lock, necessários para realizar a ação;
  • Realizar os processos só com estes dados;
  • Ir ao banco de dados (com o mínimo de interações) para realizar operações que exigem lock;
  • Não manter a transação aberta por muito tempo, para minimizar contenções e locks.

Minimize o uso de Stored Procedures

Este é um ponto aberto a discussão, depende muito de cada caso. Não é uma regra geral, existem pontos em um sistema onde uma stored procedure realmente faz diferença, mas seu uso excessivo ou como regra geral para tudo impõe outros custos e consequências. O Princípio 1 diria: “use apenas stored procedures”. No entanto, esta decisão pode causar grandes problemas para o Princípio 2 devido à escalabilidade. As Stored procedures têm a vantagem de ser pré-compiladas, e não há nada mais perto de um dado no Banco de Dados.

Porém Bancos de Dados transacionais são especializados em quatro funções: Sort, Merge, gerência de Locks e Log. A gerência de lock é uma das tarefas mais críticas para a implementação de algoritmos distribuídos, e este é o real motivo de existirem poucos Bancos de Dados que possam implementar a escalabilidade horizontal. Se as máquinas de Banco de Dados têm dificuldade de escalar horizontalmente, ela é um recurso escasso e precioso. Temos então que otimizar seu uso para não consumir excessivamente seus recursos a ponto de onerar os demais processos do ambiente. Isto acaba adiando a necessidade de escalar horizontalmente o SGBD.

Utilize Pool de objetos e Cache quando cabível

Os Princípios 1, 5 e 6 nos levam ao uso de Pool e Cache. Muitos dados são lidos com muita freqüência e são raramente modificados (um exemplo é o uso de tabelas de metadados e de certos cadastros básicos). Gastar tempo, banda de rede e CPU do Banco de Dados para trazê-los fere os Princípios 1 e 5. Melhor trazê-los no início (Princípio 6) e garantir que estão locais (Princípio 1).

No entanto, alguns cuidados devem ser levados em conta:

  • Não use espaço demasiado da memória – contradizendo o Princípio 5 para o item memória.
  • Se houver escrita e precisarmos de transacionamento, estaremos incorrendo no problema de sincronização e lock entre vários caches – já que um cache naturalmente introduz afinidade (ver Princípio 3). Se a política de renovar o cache de tempos em tempos não for suficiente, o melhor, neste caso, é usar o Banco de Dados. Deve-se preferencialmente usar o cache para leituras de dados com baixa volatilidade.

Objetos que custam caro para inicializar merecem uma técnica semelhante: o pool. Nele, vários objetos são pré-inicializados, e ficam à espera de chamadas. Uma vez chamados, ficam alocados à tarefa corrente até que, uma vez liberados, retornam ao pool. Esta implementação é conhecida também por pool de processos ou “Working Threads” .

Use, quando possível, técnicas assíncronas

Filas podem ser criadas por bons motivos: seja para manter a consistência de dados (ex.: filas de contenção devido à locks), ou para diminuir o risco do uso indiscriminado de recursos.O uso de técnicas assíncronas, quando permitido pelas regras de negócio, traz algumas oportunidades de otimização no uso de recursos. Com o enfileiramento de mensagens em sistemas de filas (Message Queues) , podemos alocar recursos limitados e suficientes para o tratamento das mensagens. Com isto, poderemos consumir aos poucos os elementos da fila sem aumentar os recursos em momentos de pico.

Por exemplo, disponibilizamos 10 threads para tratamento de um tipo de requisição e não estaremos usando mais recursos caso haja um pico de requisições. O tempo médio do tratamento será maior (devido ao tempo de espera), mas estaremos garantindo um limite no uso dos recursos (Princípio 5), além de permitir por exemplo que operações secundárias decorrentes de um processamento de interface, que normalmente precisa ser o mais rápido possível, possam ser realizados após o processamento principal ter sido completo e seu retorno será mais rápido ao cliente ou solicitante do processo enquanto a etapa secundária será processada na fila quando chegar a sua vez.

Conclusão

Muitas outras técnicas de performance podem ter seu impacto medido de acordo com os princípios apresentados. Meu conselho é tê-los em mente em toda decisão de design a ser feita em um projeto. O balanceamento no uso destes princípios pode significar o sucesso ou fracasso da sua aplicação. Lembre-se que cada caso de uso pode demandar o uso de uma ou mais técnicas, cada caso é um caso.

Lembre-se também que você não precisa de performance optimizada em todos os momentos ou rotinas do sistema. Como a exigência de performance pode levar a técnicas de razoável complexidade, na implementação ou refactoring, você poderá estar onerando em demasia o seu projeto. Por isto, estabeleça na fase de requerimentos os pontos críticos de performance a serem obtidos. Com isto, você poderá priorizar questões como reaproveitamento e facilidade de manutenção e utilizar técnicas de performance onde realmente necessário.

Até o próximo post 🙂