Com a correta utilização de frameworks como o Fork/Join, podemos melhorar o desempenho e a agilidade na escrita de códigos e sua execução em computadores multicores.
A evolução do poder de processamento de computadores e dispositivos cresceu muito em relação ao que possuíamos alguns anos atrás. Por volta de 1965, ainda não existia uma expectativa real do mercado de hardware e seu crescimento, até que Gordon E. Moore, na época presidente da Intel, fez uma profecia sobre a capacidade de processamento, a qual dobraria a cada 24 meses e mantendo o mesmo custo, que ficou conhecida como lei de Moore. Poucos anos depois foi possível afirmar que a sua previsão estava correta e o padrão se mantém até os dias atuais.
Desta forma, por muitos anos, desenvolvedores de aplicativos têm se aproveitado desse progresso, não se preocupando em realizar melhorias no código para aumentar o desempenho e eficiência do software. Assim, os sistemas começaram a ficar cada vez mais rápidos apenas com a troca do hardware e as novas tecnologias de processador.
Nessa evolução do hardware, o aumento ficou direcionado ao clock do processador, isto é, à frequência que o processador executa as tarefas, passando de 500MHz, 1GHz, até os atuais processadores de 3GHz.
No entanto, o aumento da velocidade impacta em um aumento no consumo de energia e a dissipação de calor, o que vai na contramão da produção de processadores cada vez mais compactos para dispositivos também cada vez menores e que tenham um aproveitamento eficiente de energia.
Assim, não seria mais possível aumentar a velocidade dos processadores. Logo, a solução seria aumentar a quantidade de núcleos nos processadores. Essa transformação se deu com o surgimento da tecnologia Multicore. Com essa tecnologia, um único chip é capaz de acomodar duas ou mais unidades de processamento.
Para o sistema operacional, cada núcleo é visto como um processador diferente e a adição de novos núcleos de processamento permite que as instruções das aplicações sejam processadas em paralelo, em vez da execução sequencial.
Com essa nova realidade, desenvolvedores de aplicativos, para acompanhar essa mudança de paradigma, precisarão alterar a maneira que escrevem seus códigos, passando da abordagem sequencial para a paralela.
Felizmente, a linguagem Java desde o seu início, foi projetada para dar suporte ao desenvolvimento de aplicações multitarefa e a facilitar essas implementações por parte dos desenvolvedores.
A utilização de Threads, para criação de tarefas que deveriam executar ao mesmo tempo, foi e ainda continua sendo umas das maneiras mais simples de se escrever uma aplicação concorrente.
Com o crescimento da linguagem, novas características foram adicionadas visando melhorar o tratamento de tarefas concorrentes. A primeira grande mudança aconteceu na versão 5.
Até então era o programador que controlava a criação, execução e o estado das threads. Com o Java 5 e a inclusão do framework Executor, a criação, gerência e finalização das threads passou a ficar a cargo da API.
Posteriormente no Java 7, a novidade veio com o lançamento do framework Fork/Join, que ajuda a simplificar ainda mais escrita de código paralelo, permitindo o aproveitamento de todos os recursos oferecidos por múltiplos processadores.
E em sua versão mais recente, o Java 8, com os novos recursos de programação funcional, a linguagem permite a produção de código paralelo, limpo e com qualidade.
Logo, o conhecimento e entendimento do funcionamento e desenvolvimento de aplicações paralelas deixa de ser apenas um diferencial e passa a ser uma necessidade.
Com base nisso, este artigo irá descrever os conceitos da programação paralela, analisando e utilizando as ferramentas disponibilizadas pela linguagem Java.
Arquiteturas paralelas
A capacidade de processar informações e retornar resultados é o que torna os computadores a ferramenta ideal para auxiliar na resolução de cálculos e problemas complexos.
Entretanto, quanto maiores os problemas ou a quantidade de informações a serem processadas, mais recursos serão necessários, assim como, computadores com maior poder de processamento.
Com o intuito de solucionar ou amenizar esse problema, surgiram algumas arquiteturas computacionais que buscam paralelizar a execução de tarefas complexas e que demandam muito tempo. Para isso, essas arquiteturas são capazes de dividir um mesmo problema em problemas menores para serem resolvidos individualmente. Dentre essas arquiteturas, nos subtópicos a seguir analisaremos algumas das mais utilizadas.
Arquitetura com multiprocessadores
Uma maneira de se adquirir o paralelismo é utilizar uma arquitetura de múltiplos processadores. Na Figura 1, cada P representa uma unidade de processamento, que se comunica com a memória principal (M).
A memória principal, por sua vez, é compartilhada entre todos os processadores, podendo ser única ou dividida em vários módulos. O acesso à memória principal é realizado através da rede de interconexão.
Arquitetura com multicomputadores
Outra forma de arquitetura paralela é a de multicomputadores. Neste modelo, cada unidade de processamento tem sua própria memória principal.
Desta forma, como observamos na Figura 2, o processador P1 se liga com a memória principal M1. Nesta proposta, nenhum processador poderá ter acesso diretamente a um módulo de memória que não esteja diretamente relacionado a ele.
Assim, a comunicação entre as unidades computacionais é realizada por troca de mensagens através da rede de interconexão.
A vantagem dessa arquitetura está na possibilidade de se adicionar um número grande de computadores à rede. Entretanto, o aumento de desempenho em nível de aplicação irá depender do desenvolvimento do software paralelo.
Arquitetura multicore
Para obtermos melhor desempenho podemos aumentar o número de processadores utilizando a arquitetura de
Multicomputadores. Esta solução é adotada por várias empresas que possuem grandes datacenters, como Facebook, Google
e Microsoft, que precisam processar uma grande quantidade de informações no menor tempo po ...