Computação Paralela e Arquitetura Distribuída
Autor: Adilson Benevides | Publicado: 2011-05-23T09:54:00-07:00

Computação ParalelaEste capítulo procura abranger os fundamentos da computação paralela de forma prática e objetiva, norteado por dois aspectos fundamentais: fornecer o embasamento teórico básico para o capítulo cinco e preconizar a utilização desse tipo de computação em projetos de engenharia da produção, nas suas mais diversas áreas. Descreve, sob o ponto de vista do usuário, os principais e mais representativos modelos arquitetônicos dos sistemas distribuídos existentes e, dentro dessa mesma visão, estuda as características principais das linguagens de programação paralela disponíveis, suas classes de aplicações, os suportes por elas requeridos e como essas linguagens os atendem.4.1 MotivaçãoA programação paralela pode ser considerada como sendo a atividade de se escrever programas computacionais compostos por múltiplos processos cooperantes, atuando no desempenho de uma determinada tarefa Andrews (1991). Embora ainda conceitualmente intrigante, a computação paralela já se torna essencial no projeto de muitos sistemas computacionais, seja pela própria natureza inerentemente paralela apresentada por um sistema, seja na minimização do tempo de processamento, ou mesmo na busca de uma estruturação melhor e ou mais segura de um sistema.No desenvolvimento de um sistema paralelo, após a especificação do problema a ser resolvido, deve-se decidir quantos processos podem compor o mesmo e como esses processos irão interagir. Essas decisões são diretamente afetadas pela natureza da aplicação e pelas características do hardware que se tem disponível.Uma vez projetado o sistema, definidos os processos componentes e suas interações, surge um problema crucial: a garantia de que a comunicação entre os processos seja adequadamente sincronizada. Os diversos ambientes disponíveis para o desenvolvimento de sistemas distribuídos e as diferentes maneiras como os processos de um programa paralelo podem ser criados e coordenados são os principais temas deste capítulo.Tal como em outras áreas empíricas da ciência da computação, a história da computação paralela não poderia ser diferente, ela surgiu em decorrência de desenvolvimentos marcantes verificados em hardware. Dentre os principais precursores desse tipo de programação, destacam-se os sistemas operacionais. Com a importante independência dos controles de dispositivos, alcançada na década de 1960, o processador central de um computador deixou de interagir diretamente como os dispositivos de entrada e saída, mas através de interfaces especializadas. Desta forma, pôde-se constatar que a utilização do processador central poderia ser consideravelmente otimizada, utilizando-se os seus tempos ociosos, decorrentes da espera pelas realizações das operações independentes de entrada e saída. Esses intervalos de tempo ociosos poderiam ser, por exemplo, utilizados no processamento de outros programas. Alguns sistemas operacionais começaram, então, a ser projetados como uma coleção de processos, em um ambiente em que as tarefas de usuários conviveriam harmoniosamente com os processos gerentes de dispositivos e os demais processos do sistema operacional Silberschatz e Peterson (1994) e Tanenbaum (1992). Em sistemas dessa natureza, onde um único processador é capaz de processar mais de um programa simultaneamente, conhecidos como sistemas multiprogramados, os processos são executados em fatias de tempo, que o processador a eles dedica de forma intercalada.Assim, o despertar da idéia de se construir sistemas computacionais como coleção de processos cooperantes e a evolução tecnológica fortemente verificada na área da microeletrônica foram os principais fatores responsáveis pelo surgimento de uma grande variedade de arquiteturas compostas por múltiplos processadores, conhecidos por sistemas multiprocessados.4.2 Sistemas Computacionais DistribuídosNão há um consenso absoluto na literatura com relação a definição de sistema computacional distribuído havendo, no entanto, dentre as muitas definições encontradas, pelo menos um ponto em comum: todas requerem a presença de múltiplos processadores. A considerável controvérsia reinante pode, portanto, estar relacionada com o grande número de diferentes modelos arquitetônicos de sistemas, com múltiplos processadores propostos e construídos, abrangendo um grande leque de opções em termos de objetivo do projeto a ser executado, do seu tamanho e desempenho. Em um sistema multiprocessado com memória compartilhada múltiplos processadores compartilham uma memória comum. Já em um sistema multicomputador, vários processadores, com memórias privadas, conhecidos como nós, são conectados através de um hardware especializado que envia mensagens entre eles, em alta velocidade. Em um sistema de rede de computadores, processadores são conectados através de uma rede de comunicação que pode ser local ou de longa distância. Muitos sistemas de comunicações híbridos também podem ser encontrados, como, por exemplo, uma rede de workstations multiprogramadas. Deve-se, também, incluir nesse conjunto de sistemas, os computadores construídos especialmente para aplicações numéricas de computação intensiva: os computadores vetorizados, que utilizam vários processadores em aplicações simultâneas de operações aritméticas idênticas, sobre diferentes dados. Outros sistemas desse tipo são os chamados dataflow e os reduction machines, que aplicam simultaneamente diferentes operações à diferentes dados.É importante salientar que os sistemas computacionais distribuídos existentes não diferem somente nas suas concepções arquitetônicas, eles também divergem, consideravelmente, nas formas como são programados. Alguns fazem uso de linguagens de programação convencionais, suplementando-as, na maioria das vezes, com rotinas de bibliotecas, que garantam a geração e a coordenação de processos. Outros sistemas já são programados em linguagens completamente novas, especificamente projetadas para aplicações distribuídas.Muitos estudiosos do assunto advogam que todas as configurações aqui mencionadas podem ser enquadradas na categoria de sistemas distribuídos, outros incluem somente os computadores geograficamente distribuídos, conectados por uma rede de longa distância.Não obstante o mérito dessa questão fuja da alçada desse trabalho, é fundamental que sejam estabelecidas definições para sistema e, principalmente, programa distribuído, visto que esses conceitos serão utilizados no capítulo quatro, e o projeto e implementação de um programa desse tipo, são objetivos finais neste trabalho. Para tanto será adotada a definição de sistemas distribuídos como a encontrada em Bal et al. (1989), que considera um sistema de computação distribuído como consistindo de múltiplos processadores autônomos, que não compartilham memória primária, mas cooperam entre si, através de envio de mensagens sobre uma rede de comunicação. Cada processador em um sistema desse tipo executa seu(s) próprio(s) fluxo(s) de instrução e utiliza sua própria área de dados locais, ambos armazenados em sua memória local particular. Para programas distribuídos será adotada a definição proposta em Andrews (1991), que define um programa distribuído como sendo um programa concorrente (ou paralelo) no qual os processos componentes comunicam-se através de passagens de mensagens. Muito embora o nome leve o adjetivo "distribuído", decorrente do fato de que esses programas tipicamente são executados em ambientes distribuídos, um programa desse tipo, no entanto, também pode ser executado em um sistema de multiprocessadores com memória compartilhada, ou até mesmo em um monoprocessador.Uma complementação da caracterização de sistemas distribuídos está fundamentada na comunicação. Em função do tipo de comunicação utilizada entre os processadores de um sistema distribuído, foram criadas duas categorias de sistemas. Uma arquitetura distribuída é dita ser fortemente acoplada se a comunicação é rápida e confiável e, também, se os seus processadores se encontram fisicamente próximos uns dos outros. Como exemplos podem ser citados as grelhas e os hipercubos, etc., que utilizam uma rede de comunicação, consistindo de ligações ponto a ponto, confiável e rápida, a qual conecta cada processador a algum subconjunto de outros processadores. Já uma arquitetura distribuída é dita ser fracamente acoplada se a comunicação entre os seus processadores, fisicamente dispersos, apresente baixa velocidade e pouca confiabilidade. Exemplos de sistemas dessa classe são as redes de longa distância ou locais de estações de trabalho, que permitem comunicação direta entre dois processadores quaisquer, porém com baixa velocidade e não totalmente confiável. Ocasionalmente uma mensagem pode ser danificada, chegar fora de ordem ou, simplesmente, não alcançar o seu destino. Nesses casos, devem ser utilizados protocolos para implementar comunicações mais seguras.Geralmente as aplicações que utilizam sistemas distribuídos fortemente acoplados, apresentam altas taxas de comunicação entre os seus processos, pois a assiduidade de intercomunicação entre as unidades de paralelismo é grande. Essas aplicações são ditas possuírem granularidades finas. Por outro lado, as aplicações com baixa granularidade, formadas por processos com baixo nível de interação, podem ser suportadas por sistemas distribuídos fracamente acoplados.4.3 Arquiteturas de MulticomputadoresUma vez que se pretende utilizar um sistema distribuído do tipo multicomputador para a implementação do sistema proposto nesse trabalho, será então abordada nesta seção, com mais detalhes, a categoria dos multicomputadores, ver Duncan (1990) e Reed (1987).Como já visto anteriormente, em um multicomputador a interação entre os seus nós (processadores autônomos com memórias privadas, de onde são obtidas suas instruções e dados) é alcançada, única e exclusivamente, através do envio de mensagens que trafegam pelo sistema por meio de uma rede de interconexão de alta velocidade. Essas redes, que representam o principal elemento determinante do desempenho de um sistema multicomputador, podem ser divididas em duas classes principais: as redes de interconexão de barramento e as redes de interconexão bipontuais.4.3.1 Redes de Interconexão de BarramentoAs redes de barramento (Figura 4.1) que compreendem o meio de conexão mais simples e de menor custo, apresentam uma grande flexibilidade no tocante ao número de conexões existentes, Feng (1992). Esse modelo permite adição e subtração de nós, sem a necessidade de alterações severas no sistema. Uma conseqüência imediata dessa propriedade está relacionada com o fator de tolerância à falhas, característica importante e desejável em muitos sistemas dessa natureza. Uma eventual falha de um ou até de mais nós não afetará diretamente as atividades dos demais elementos do sistema.Figura 4.1 Exemplo de multicomputador baseado em barramentoUma das principais desvantagens dos multicomputadores baseados em rede de barramento está relacionada com a extensibilidade, a qual fica limitada à capacidade do barramento utilizado e cuja sobrecarga pode conduzir à degradação do desempenho do sistema. Pode também ser apontada como uma outra desvantagem considerável, relacionada com o fator tolerância à falhas, a interrupção da comunicação entre todos os nós, decorrente de uma eventual falha no barramento.4.3.2 Redes de Interconexão BipontuaisAs redes bipontuais são constituídas de um conjunto de canais de comunicação que interligam os nós dois a dois. As mais diversas variedades de topologias de redes de interconexão, que podem ser construídas com base nesse modelo, recaem em duas classes básicas que são as redes estáticas e as redes dinâmicas.4.3.2.1 Redes Bipontuais EstáticasComo o próprio nome indica, esse tipo de rede caracteriza-se por apresentar uma topologia fixa, as interconexões dos nós são estabelecidas na construção do sistema e se tornam imutáveis. A topologia ideal de um sistema baseado nesse modelo seria representada pelo grafo completo (Figura 4.2), no qual cada nó encontra-se conectado a todos os demais. Na prática, no entanto, a implementação de um sistema como esse é, muitas vezes, inviável, uma vez que o número de canais necessários para interligar n nós é igual a n2.Figura 4.2 Grafo CompletoRedes baseadas no modelo bipontual estática são normalmente construídas com números menores de canais, obrigando que a comunicação entre os nós seja efetuada de maneira indireta e fazendo com que as mensagens percorram nós intermediários até alcançar o nó destino. Serão mostrados, a seguir, dois exemplos de topologias de redes de interconexão, com número restrito de canais: a grelha e o hipercubo. A topologia conhecida como grelha é bidimensional e seu custo de implementação se torna muitas vezes viável uma vez que o número de canais cresce linearmente em relação ao tamanho da rede, Figura 4.3.Figura 4.3 Topologia do tipo grelhaO principal problema apresentado por esse tipo de topologia, no entanto, reside no custo de transmissão de uma mensagem entre dois nós não vizinhos. Considerando o pior caso, em que uma mensagem deva ser enviada de um extremo a outro da rede, por exemplo, entre os nós situados no canto superior direito e inferior esquerdo, é necessário que a mensagem percorra 2 n1/2 canais, onde n é o número total de nós da rede. Nos casos médios, o número de canais que devem ser percorridos é aproximadamente a metade do pior caso, tornando esta topologia proibitiva para grandes redes Tanenbaum (1992).Dessa forma, constata-se esforços concentrados na pesquisa e no desenvolvimento de novas topologias, cujos custos médios de transmissão não cresçam proporcionalmente com o tamanho da rede; modelos como os hipercubos são produtos desses esforços. Um hipercubo ou n-cubo Reed (1987) é uma topologia caracterizada pelo parâmetro n, o qual representa sua dimensão e é igual para todos os nós de um sistema. Um hipercubo de dimensão zero é formado por apenas um nó processador. Um hipercubo de dimensão n+1 é formado a partir da composição de dois hipercubos de dimensão n. A figura 4.4 ilustra hipercubos de dimensões 1, 2, 3 e 4.     Figura 4.4 Exemplos de hipercubosO número de nós percorridos por uma mensagem, no pior caso, para uma arquitetura do tipo hipercubo, composta por n nós, é igual a log2 n. Embora o hipercubo proporcione um ganho significativo no custo de transmissão quando comparado à grelha, ele não se constitui em uma arquitetura ideal, uma vez que o número de canais conectados à cada nó deve ser igual à sua dimensão.4.3.2.2 Redes Bipontuais DinâmicasAs redes dinâmicas não possuem uma topologia fixa, como as estáticas. As ligações entre os seus nós processadores são obtidas por meio de comutadores de conexões que lhes proporcionam uma maior flexibilidade. Uma rede desse tipo se amolda a uma gama maior de aplicações, uma vez que pode ser configurada de acordo com as características de comunicação de um determinado problema. A seguir, serão abordados, sem maiores detalhes, dois exemplos clássicos de redes desse modelo: as redes dinâmicas baseadas em crossbar e as redes dinâmicas multi-estágio.Um crossbar é um dispositivo de hardware cujo objetivo é comutar conexões entre um conjunto de processadores, propiciando conexões entre quaisquer pares de processadores desse conjunto, Broomell (1983). Um crossbar com m entradas e n saídas permite a conexão de quaisquer entradas a quaisquer saídas, sendo possível, no entanto, para cada linha de entrada ou saída, a participação de uma única conexão de cada vez. A organização deste dispositivo é ilustrada na Figura 4.5, onde as entradas estão dispostas verticalmente e as saídas horizontalmente, constituindo uma grelha, na qual cada interseção de duas linhas representa um comutador, que controla a conexão entre a entrada e a saída correspondentes. Na mesma figura é destacada uma conexão entre a entrada E4 e a saída S3, resultante do fechamento do comutador correspondente. Nessa situação, a realização de uma nova conexão, envolvendo a entrada E4 e a saída S3, não seria possível.Figura 4.5 Exemplo de um CrossbarO número de comutadores em um crossbar com n dispositivos a serem conectados é igual a n2. Outras classes de redes dinâmicas estão sendo pesquisadas, buscando minimizar o número de comutadores utilizados. Por exemplo, as redes chamadas multi-estágio, é uma proposta nesse sentido, ver Hwang (1993) e Youn (1993).As redes multi-estágio são, normalmente, formadas através da combinação de múltiplos elementos que possuem a capacidade de conectar duas entradas à duas saídas, simultaneamente. A partir dessa combinação, pode-se construir redes dinâmicas com capacidade de conectar quaisquer elementos.É importante salientar que dentre os sistemas distribuídos caracterizados como o mais fortemente acoplado e o mais fracamente acoplado, existe uma grande variedade de sistemas computacionais distribuídos. Muito embora a velocidade e a confiabilidade de comunicação desses sistemas diminuam de um extremo a outro, todos se ajustam a um modelo básico comum em que processadores autônomos, conectados por algum tipo de rede, comunicam-se por envio de mensagens. Devido a essa similaridade conceitual, as linguagens de programação utilizadas por esses sistemas não são fundamentalmente diferentes e, em princípio, qualquer uma dessas linguagens pode ser utilizada na programação de uma grande variedade de arquiteturas distribuídas. A escolha de uma linguagem mais adequada fica, portanto, muito mais em função da natureza da aplicação a ser implementada.A seguir, serão vistas as principais classes de aplicações que têm sido desenvolvidas para sistemas distribuídos, quais os tipos de suportes requeridos e como esses suportes podem ser providos por uma linguagem de programação distribuída, Coulouris (1994).4.4 Classes de Aplicação de Sistemas DistribuídosA computação é uma ciência em constante e vigorosa evolução. Suas aplicações, que se alastram pelas mais diversificadas áreas de atuação humana, vêm incessantemente promovendo a busca por sistemas computacionais capazes de oferecer, cada vez mais, maior desempenho e alta confiabilidade. Um dos resultados efetivos alcançados, fruto dessas investigações, é o modelo arquitetônico composto por múltiplos elementos processadores, que veio propiciar os sistemas distribuídos. Sistemas dessa natureza têm-se revelado excelente alternativa, proporcionando um grande poder computacional através da exploração de eventos potencialmente paralelos, encontrados em grande parte das mais diversas aplicações. Esses sistemas, comparados aos sistemas convencionais, podem apresentar uma melhor relação custo-desempenho e incrementos consideráveis de confiabilidade.De maneira geral, as razões que justificam as aplicações de sistemas distribuídos podem ser classificadas em quatro principais categorias, ver Bal et al. (1989):· redução do tempo de processamento; · aumento da confiabilidade e da disponibilidade; · melhoria na estruturação do sistema e · aplicações inerentemente distribuídas.Em muitos sistemas computacionais, a velocidade de processamento pode-se tornar um fator determinante. Através da exploração das partes de um programa que, potencialmente, possam ser executadas em processadores diferentes, simultaneamente, pode-se alcançar redução significativa no tempo de processamento. Um exemplo típico dessa categoria de aplicações é o sistema desenvolvido nesse trabalho, onde um espaço de soluções possíveis para um determinado problema da produção industrial, deve ser vasculhado por vários processos cooperantes, simultaneamente, almejando uma redução relevante no tempo gasto na determinação de uma boa solução .Sistemas computacionais distribuídos são potencialmente mais confiáveis do que os tradicionais uma vez que possuem a, assim chamada, propriedade de falha parcial, ou seja, uma vez que os processadores são autônomos, uma falha em um deles não afetará o funcionamento normal dos demais processadores. Para aplicações envolvidas com situações críticas, onde a queda do sistema implica em danos irreparáveis, tais como controle de aeronaves, automatização industrial, etc, essa propriedade é altamente desejável. Dessa forma, confiabilidade pode ser incrementada pela replicação de funções ou dados críticos da aplicação, em vários processadores.Muitas vezes a implementação de uma certa aplicação se torna mais natural se estruturada como uma coleção de serviços especializados. Cada serviço podendo utilizar um ou mais processadores dedicados, alcançando com isso maior desempenho e alta confiabilidade. Os serviços podem enviar requisições de tarefas a outros através de uma rede. Se uma nova função necessitar ser incorporada ao sistema, ou mesmo, se uma função já existente necessitar de uma computação mais potente, torna-se relativamente fácil adicionar novos processadores. Um exemplo dessa categoria de aplicação de sistemas distribuídos é o Amoeba, um sistema operacional distribuído, que garante um excelente compartilhamento de recursos computacionais, através da implementação de um serviço de arquivos, um serviço de impressão, um serviço de processos, um serviço de terminais, etc, Mullender (1990).Finalmente, há aplicações que se caracterizam como inerentemente distribuídas e, assim, a utilização de sistemas distribuídos é natural. Por exemplo, uma companhia com múltiplos escritórios e fábricas pode necessitar de um sistema distribuído de forma que pessoas e máquinas, em diferentes locais, possam se comunicar.4.5 Suportes para Programação DistribuídaApós a descrição dos modelos arquitetônicos distribuídos existentes e os principais tipos de aplicações para esses sistemas, serão abordadas as diferentes formas como essas aplicações podem ser implementadas, Andrews (1991).A programação distribuída, aqui referenciada como a atividade de implementação de uma aplicação em um sistema distribuído, requer pelo menos dois requisitos básicos essenciais, inexistentes na programação seqüencial:· distribuição de diferentes partes de um programa entre os, possivelmente, diferentes processadores e · coordenação dos processos componentes de um programa distribuído.Em qualquer uma das categorias de aplicações já mencionadas, um programa distribuído conterá trechos de código que deverão ser processados em paralelo, em diferentes processadores. O primeiro e primordial requisito para que a programação distribuída possa existir é, portanto, a habilidade de se atribuir diferentes partes de um programa à diferentes processadores.O segundo requisito, não menos importante, diz respeito a coordenação dos processos que compõem um programa distribuído. A comunicação e o sincronismo são fatores indispensáveis para que os processos possam trocar informações e resultados intermediários e sincronizar suas ações.Um terceiro requisito do suporte para programação distribuída, que deve ser apontado, particularmente importante para as aplicações tolerantes à falhas, é a habilidade de se detectar uma falha em um dos processadores e recuperá-la em tempo hábil suficiente para se evitar a queda geral do sistema.O suporte para implementação de aplicações distribuídas, que idealmente deveria contemplar a todos os três requisitos acima aludidos, pode ser provido pelo sistema operacional ou pela linguagem especialmente projetada para programação distribuída.No primeiro caso, as aplicações são programadas em uma linguagem seqüencial tradicional, estendida com rotinas de biblioteca que invocam primitivas do sistema operacional. Uma vez que as estruturas de controle e os tipos de dados de uma linguagem seqüencial não são adequadas à programação distribuída, esse enfoque muitas vezes pode gerar situações conflitantes, tais como nos dois exemplos considerados a seguir.Ações triviais, como a criação de um processo ou mesmo o recebimento de uma mensagem de um remetente específico, podem ser perfeitamente expressas através de chamadas de rotinas de biblioteca, relativamente simples. Problemas surgem, no entanto, em situações mais complexas onde, por exemplo, um processo que deseja selecionar o recebimento de mensagens provenientes de alguns processos específicos, onde o critério de seleção dependa, por exemplo, do estado do receptor e do conteúdo da mensagem. Para que o sistema operacional consiga realizar um pedido como esse, provavelmente, um grande número de chamadas complicadas de biblioteca deverá ser necessário. Não deixando de considerar, também, o lado do programador que, com certeza, encontrará severas dificuldades para conseguir expressar sua lógica de programação, em uma linguagem que não apresente notações concisas para tais situações.Problemas graves com tipos de dados podem surgir em circunstâncias nas quais, por exemplo, uma estrutura de dados complexa deva ser passada como parte de uma mensagem, para um processo remoto. O sistema operacional, desconhecendo a forma como os dados estão representados, é incapaz de converter essa estrutura em pacotes - seqüências de bytes - para enviá-los pela rede. Novamente, o programador é quem deverá escrever o código explicitamente, arranjando a estrutura de dados no envio da mensagem, em uma seqüência de bytes, de tal forma que possa ser reconstituída no seu recebimento. Por outro lado, uma linguagem especificamente projetada para programação distribuída, faria essa conversão automaticamente, além de oferecer outras importantes vantagens na programação, tais como, melhor legibilidade, portabilidade e verificações em tempo de compilação.Normalmente, os serviços de cooperação entre processos oferecidos pelos sistemas operacionais são extremamente simples, podendo ser sintetizados em duas primitivas, ver Ben (1985):· SEND (mensagem): envia uma mensagem à um processo destinatário e· RECEIVE (mensagem): recebe uma mensagem de um processo origem.Dependendo do sistema operacional, estas primitivas podem ter efeito bloqueante ou não. Um bloqueio, normalmente, se estende até que a operação seja efetivamente realizada, caracterizando assim uma forma primitiva de sincronização.A rigidez desses serviços oferecidos pelos sistemas operacionais constitui o aspecto mais relevante no contraste entre os suportes oferecidos pelos sistemas operacionais, e as linguagens de programação distribuída. Grande parte das linguagens construídas para esse fim apresentam modelos de programação de níveis mais elevados, possibilitando que o programador tenha um poder de abstração muito maior do que naqueles oferecidos pelo modelo simples de troca de mensagens, suportado pela maioria dos sistemas operacionais.4.6 Modelos de Linguagens de Programação DistribuídaUm grande número de linguagens para programação distribuída surgiu na década 1980, dificultando até a tarefa da escolha da linguagem mais adequada para uma determinada aplicação, principalmente porque os seus modelos básicos de programação dessas linguagens diferenciavam consideravelmente.O modelo de programação distribuída considerado básico é aquele no qual um conjunto de processos seqüenciais rodam em paralelo, comunicam-se por meio de passagem de mensagens e a detecção de falhas é feita explicitamente Bal et al. (1989). Este padrão reflete diretamente a concepção de sistemas distribuídos, ou seja, um conjunto de processadores autônomos conectados por meio de uma rede de comunicação. Para muitas aplicações, esse modelo de processos e passagem de mensagens é o suficiente e muitas vezes até vantajoso, pois ele pode ser eficientemente mapeado sobre a arquitetura distribuída, munindo o programador do controle total sobre os recursos de hardware (processadores e rede) disponíveis.Entretanto, modelos alternativos de programação distribuída vêm sendo projetados pois, para muitas aplicações que exigem níveis de abstração maiores, o padrão básico se torna insuficiente.A primitiva de comunicação, originalmente baseada em passagem de mensagens, também passou a ser questionada e novos padrões, que não necessariamente refletem diretamente o modelo de comunicação do hardware, começaram a surgir. Dentre os modelos alternativos que maiores impactos causaram ao padrão original de comunicação entre processos, destacam-se os modelos baseados em chamada remota de procedimento e em dados compartilhados, Finkel (1980). Este último, aparentemente paradoxal, já que em um sistema distribuído não existem memórias compartilhadas, merece algumas considerações. Assim como o hardware de um sistema pode ser fisicamente distribuído ou não distribuído, o software também possui uma distinção similar. No caso de sistemas de software, a distinção concerne na distribuição lógica dos dados utilizados, ao invés da distribuição física das memórias. Define-se um sistema de software logicamente distribuído como aquele consistindo de múltiplos processos que se comunicam através de passagem de mensagens explícitas. Um sistema de software logicamente não distribuído seria, portanto, aquele no qual os processos se comunicam por meio de dados compartilhados. Considerando isoladamente os sistemas de hardware e software, quatro combinações viáveis diferentes podem ocorrer, gerando as classes de sistemas abaixo, ver Bal et al. (1989):· software logicamente distribuído, rodando em hardware fisicamente distribuído; · software logicamente distribuído, rodando em hardware fisicamente não distribuído; · software logicamente não distribuído, rodando em hardware fisicamente distribuído e · software logicamente não distribuído, rodando em hardware fisicamente não distribuído.A primeira classe é a normal, uma coleção de processos, cada um rodando em um processador diferente e se comunicando através das primitivas SEND e RECEIVE, que enviam mensagens, por exemplo, utilizando uma rede. A segunda com a mesma estrutura lógica de multiprocessos da anterior, simula a passagem física de mensagens através de passagem lógica de mensagens, utilizando memória compartilhada. A terceira classe esconde a distribuição física do sistema de hardware, simulando memória compartilhada. A última também utiliza comunicação por dados compartilhados, porém nesse caso a existência de memória fisicamente compartilhada facilita a implementação.Finalmente, o terceiro aspecto importante que deve ser considerado nos modelos das linguagens de programação distribuída, além do paralelismo e da comunicação, é o tratamento de falhas de processadores. O problema crucial nessa questão é conseguir trazer, após a detecção de uma falha através de algum mecanismo utilizado, o sistema de volta a um estado consistente. Esta recuperação normalmente pode ser realizada se as quedas de processadores forem previstas e as devidas precauções forem tomadas durante a computação normal do sistema. Por exemplo, cada processador, em intervalos regulares, deve descarregar seu estado interno em uma memória secundária. Modelos alternativos estão sendo propostos para oferecer, ao programador, mecanismo de alto nível que permita expressar, especificamente, quais processos e dados são importantes e como eles devem ser recuperados após uma falha.A seguir, serão inspecionadas as principais e as mais representativas primitivas de linguagens de programação distribuídas, que são propostas de soluções diferentes para o tratamento do paralelismo e da comunicação entre processos.4.6.1 ParalelismoEsta seção inicia fazendo uma importante distinção entre o paralelismo real e pseudo paralelismo, Trew e Wilson (1991). A execução de um programa em um ambiente paralelo real se caracteriza pela existência de processadores em número igual ou superior ao número de processos que compõem o programa. Na execução de um programa em um ambiente pseudo paralelo, por sua vez, o número de processadores pode ser menor que o número de processos existentes, podendo chegar inclusive a um único processador. Muitas vezes é vantajoso expressar um programa como uma coleção de processos rodando em paralelo, desconsiderando o fato de que esses processos podem rodar simultaneamente. Essa técnica, naturalmente, não tem utilidade nas aplicações em que a busca pela redução do tempo de processamento é o único objetivo.Algumas linguagens escondem do programador essa distinção entre paralelismo real e pseudo paralelismo, fazendo com que o número de processos rodando efetivamente, em um dado momento, seja totalmente desconhecido. Outras linguagens não escondem essa distinção, permitindo, inclusive que o próprio programador atribua, explicitamente, determinadas partes do seu programa a determinadas unidades de processamento. Muitas vezes esse controle do mapeamento de processos em processadores é interessante, uma vez que aumenta a flexibilidade do sistema. Por exemplo, o programador pode utilizar variáveis compartilhadas na comunicação de processos que, sabidamente, estão rodando em um mesmo processador ou atribuir um determinado processador a um determinado processo por alguma razão especial.Como será visto a seguir, cada linguagem de programação distribuída possui sua unidade de paralelismo que varia do processo à expressão Hansen (1995). A granularidade do paralelismo também varia de linguagem para linguagem e depende muito do custo de comunicação apresentado pelo sistema de hardware distribuído envolvido. Por exemplo, um sistema distribuído fortemente acoplado, tal como o hipercubo, pode suportar eficientemente um paralelismo de granularidade fina. Por outro lado, um sistema de rede de longa distância inviabilizaria uma computação paralela com essa granularidade.Finalizando este preâmbulo, é interessante considerar as linguagens que não suportam o controle explícito do paralelismo. Ao invés do programador efetuar a divisão do código do programa em segmentos paralelos, é o compilador que o faz. Algumas linguagens também não permitem controle explícito da comunicação entre processos. O envio de uma mensagem a um processo resulta na geração implícita de um outro processo que gerencia essa comunicação.4.6.1.1 Expressões de ParalelismoA maneira como o paralelismo é expresso varia de linguagem para linguagem e depende muito da unidade do paralelismo adotado, que pode ser um processo, um objeto, um comando, uma expressão ou, ainda, uma cláusula, Bal et al. (1989).Para a grande maioria das linguagens imperativas, destinada à programação distribuída, o paralelismo encontra-se fundamentado na importante noção de processo. Existem várias considerações a esse respeito, mas, em geral, um processo pode ser considerado como um processador lógico que executa códigos seqüencialmente e possui seu próprio estado e dados.A definição de um processo dentro de um programa distribuído é semelhante a de um procedimento em um programa seqüencial, havendo inclusive linguagens que permitem a definição do tipo "processo".Uma vez definidos, os processos podem ser criados por duas maneiras bem distintas. Explicitamente, através de alguma construção do tipo create, que lembra uma chamada de procedimento em um programa seqüencial, com efeitos, todavia, totalmente diferentes. Enquanto que uma chamada de procedimento resulta apenas em um desvio do fluxo de execução do programa, um create inicia a execução de um novo processo, alocando-lhe um processador autônomo disponível. A criação implícita de um processo, por outro lado, é feita através da declaração de uma variável como sendo de um tipo processo, previamente definido.Em linguagens baseadas na criação implícita de processo, o número total de processos é fixado em tempo de compilação, devendo, portanto, ser previamente conhecido. Por um lado, esse aspecto é vantajoso, pois simplifica o mapeamento entre processos e processadores físicos. Por outro lado, entretanto, a criação estática de processo impõe restrição aos tipos de aplicações que possam vir a ser implementadas nessas linguagens.A existência de uma construção do tipo create pode tornar a linguagem mais flexível, pois além de permitir criação dinâmica de processos, possibilita a passagem de parâmetros ao processo recém criado. Parâmetros, normalmente, são utilizados nessa situação para estabelecer canais de comunicação entre processos.Um último aspecto considerado, relacionado com processo é a sua finalização. Um processo, normalmente, termina por si próprio, porém pode existir situações que exijam a existência de primitiva que venha abortar certos processos. Algumas providências podem também ser necessárias para se evitar que um processo tente se comunicar com um outro que já tenha terminado.Outra unidade de paralelismo considerada é o objeto. Em geral, um objeto é uma entidade auto contida que encapsula dados e comportamento e que interage com o mundo externo (outros objetos) exclusivamente por meio de alguma forma de passagem de mensagem. Os dados contidos dentro de um objeto são invisíveis fora do mesmo. O comportamento de um objeto é definido pela sua classe, que compreende uma lista de operações que podem ser invocadas através do envio de mensagens a esse objeto. Através da herança, consegue-se definir uma classe como uma extensão de uma outra já definida.As linguagens seqüenciais orientadas a objetos são baseadas no modelo de objetos passivos. Um objeto somente é ativado quando recebe uma mensagem de outro objeto, invocando alguma de suas operações. Enquanto o receptor da mensagem estiver ativo, o emissor estará esperando pelo resultado, tornando assim um objeto passivo. Após o retorno do resultado, o objeto receptor retornará ao estado passivo e o emissor para o estado ativo. Em qualquer instante do processamento, num modelo como esse, no máximo um objeto poderá estar ativo.Um ambiente paralelo pode ser obtido, a partir de um modelo seqüencial baseado em objeto, através de uma extensão que incorporasse qualquer uma das seguintes modificações:permitir a existência de objeto ativo sem que o mesmo tenha recebido mensagem;permitir que o objeto receptor continue executando após ter retornado o seu resultado;permitir que um objeto envie mensagens para vários objetos simultaneamente epermitir que o emissor de uma mensagem continue executando em paralelo com o receptor.É interessante observar que o primeiro e o segundo métodos transformam o ambiente em um modelo baseado em objetos ativos, ou seja, é atribuído a cada objeto um processo.A terceira unidade de paralelismo considerada é o comando. Algumas linguagens distribuídas expressam o paralelismo através de estruturas que agrupam comandos que devem ser processados em paralelo. Esse método, apesar de fácil utilização e entendimento, deixam muito a desejar com relação a estruturação de programa, e não apresentam construções tão gerais como outros mecanismos.A quarta unidade de paralelismo a ser considerada é a expressão das linguagens funcionais. Em uma linguagem funcional pura, as funções têm o mesmo comportamento das funções matemáticas, ou seja, calculam um resultado que depende somente dos valores dos dados de entrada, não interferindo absolutamente em outras funções do programa. Fazendo uma comparação, as linguagens imperativas permitem que as funções interfiram uma nas outras por várias maneiras: através de variáveis globais, variáveis do tipo ponteiro, etc.Desde que as funções, em uma linguagem funcional, não exerçam influência alguma umas sobre as outras, elas podem ser executadas em qualquer ordem, inclusive em paralelo. Por exemplo, na expressão abaixo:h ( f ( 10, 30 ), g ( A ) )é irrelevante se a função f for executada antes que a função g, ou não. Consequentemente as funções f e g podem, perfeitamente, ser executadas em paralelo. Em linguagens desse tipo, em princípio, todas as funções podem ser executadas em paralelo, com uma única restrição: a ordem de execução muitas vezes precisa ser considerada. Na expressão do exemplo acima, a função h necessita dos resultados das funções f e g, por conseguinte, h deve aguardar até que os resultados de f e g estejam disponíveis. Esse paralelismo implícito, verificado nas expressões desse tipo, apresenta uma granularidade fina, exigindo arquiteturas adequadas, tal como os computadores do tipo data-flow.Para sistemas distribuídos, o enfoque funcional possui alguns problemas que ainda precisam ser resolvidos. Por exemplo, a execução em paralelo de todas as funções componentes de uma expressão sem restrição, pode não ser vantajosa, principalmente nas situações em que existam várias funções cujas tarefas sejam muito simples, fazendo com que o tempo gasto na comunicação dos resultados entre processos não justifique o paralelismo empregado.A última unidade de paralelismo a ser considerada é a cláusula, tipicamente encontrada nos programas lógicos. A programação lógica também oferece, de forma natural, várias oportunidades de paralelismo. Por exemplo, os elementos figurantes localizados à esquerda de uma cláusula podem ser considerados como sendo sub teoremas a serem provados para se chegar à prova do teorema, representado pelo único elemento localizado à direita da mesma cláusula.(1) T :- T1, T2, T3, T4; (2) T :- T5, T6;Nas duas cláusulas acima apresentadas, existem duas maneiras de se implementar o paralelismo. Uma delas é trabalhar as duas cláusulas para o predicado T paralelamente, tendo como condição de parada o sucesso de uma delas ou o fracasso de ambas (conhecido como paralelismo OU). A outra maneira seria trabalhar todos os sub teoremas, das duas cláusulas, paralelamente, tendo como condição de parada o sucesso de todos ou o fracasso de algum deles (conhecido como paralelismo E).Outra interpretação que resulta na execução paralela de um programa lógico é descrever os sub teoremas em termos de processos. Por exemplo, associando um processo distinto a cada sub teorema a ser provado, na cláusula (1) do exemplo em questão, fica estabelecido que o processo para provar T seja substituído pelos quatro processos paralelos que tentam provar T1, T2, T3 e T4.Nesta seção foram descritas várias maneiras diferentes através das quais as linguagens de programação distribuída podem prover suportes para a expressão do paralelismo. Propositadamente foi dada maior ênfase a expressão que tem como unidade de paralelismo o processo, procurando, na medida do possível, não se distanciar demasiadamente dessa noção, uma vez que se pretende trabalhar com uma linguagem imperativa, que tem o processo como base.Vários padrões de interação entre processos existem na programação distribuída, Hansen (1995). Normalmente, cada um desses modelos está associado a uma técnica de programação que pode ser utilizada na resolução de um grupo de problemas semelhantes de programação distribuída. Por exemplo, no modelo conhecido por cliente servidor há dois tipos básicos de processos: clientes e servidores. Os processos clientes são assim chamados porque constantemente estão requisitando serviços aos servidores. Na maioria das vezes, um processo cliente ao requisitar um serviço ativa um servidor, o qual estava, provavelmente, bloqueado a espera de uma requisição de serviço. Em seguida, o cliente blocar-se-á, esperando a conclusão do serviço solicitado. Usualmente, um processo servidor é não terminante (possui estrutura geral em forma de um laço infinito) e é construído de forma que possa servir à vários clientes diferentes.