Logo Passei Direto
Buscar
Material
páginas com resultados encontrados.
páginas com resultados encontrados.

Prévia do material em texto

DESCRIÇÃO
A computação distribuída como base para desenvolvimento das aplicações atuais de tecnologia e sua
efetivação como campo de conhecimento da Ciência da Computação.
PROPÓSITO
Compreender o funcionamento dos sistemas distribuídos − no ponto de vista do desenvolvimento das
aplicações paralelas e distribuídas − e sua contribuição para acelerar as cargas de trabalho da atualidade,
bem como mensurar os impactos dos métodos aplicados para resolução de problemas inerentes à distribuição
de serviços, tais como: replicação, transparência, consumo de mensagens, comunicação etc.
PREPARAÇÃO
Antes de iniciar este conteúdo, tenha em mãos um ambiente de programação C: Code:: Blocks, Visual Studio,
Eclipse CDT, dentre outros.
OBJETIVOS
MÓDULO 1
Analisar os conceitos básicos dos paradigmas de comunicação
MÓDULO 2
Identificar os modelos utilizados para coordenação de tempo e para garantia da transparência de localização
MÓDULO 3
Analisar os conceitos de consistência e replicação nos sistemas distribuídos
MÓDULO 4
Identificar as principais interfaces de desenvolvimento de códigos para sistemas paralelos e distribuídos
INTRODUÇÃO
Nas últimas décadas, a taxa de crescimento do poder de processamento de minicomputadores, mainframes e
os tradicionais supercomputadores tem sido algo em torno de 20% ao ano. E a taxa de crescimento do poder
de processamento de microprocessadores e demais sistemas de apoio como GPUs − tipo de processador
envolvendo algoritmos baseados em vetores e matrizes, como os usados na área de processamento
gráfico/jogos − tem sido algo em média de 50% ao ano. O resultado desses avanços é a facilidade de
interligar diversos computadores por meio de um barramento externo de alta velocidade como uma rede local.
Podemos, então, definir uma nova era a partir dessas fusões de tecnologias que dão origem aos sistemas
distribuídos: a distribuição do processamento entre diversos computadores diferentes. Muito mais do que
subdividir tarefas, esse novo paradigma nos convida a criar tarefas computacionais mais especializadas,
conforme a natureza da função de cada computador. Um exemplo clássico é a utilização do ambiente
cliente/servidor usando comunicações por sockets, chamadas de procedimentos remotos, primitivas de
passagens de mensagens, dentre muitas outras.
Neste conteúdo, aprenderemos como é tratada a computação distribuída. Como as tecnologias convergem
para entregar uma coleção de computadores autônomos, conectados por redes − locais ou remotas −, que
aparentam aos seus utilizadores como um único sistema computacional, e em paradigmas inerentes aos
fundamentos dos sistemas distribuídos, nos quais os controles para o desenvolvimento se tornam complexos,
para criar a ilusão de um ambiente transparente, coerente, sincronizado e tolerante a falhas.
MÓDULO 1
 Analisar os conceitos básicos dos paradigmas de comunicação
TROCA DE MENSAGENS
As partes que compõem um sistema distribuído interagem pela rede para trabalhar de forma cooperativa. As
partes envolvidas trocam dados e mensagens, utilizando serviços de comunicação fornecidos pelo sistema
hospedeiro (host) e adotando protocolos de comunicação para que possam entender uns aos outros. Essa
comunicação se dá principalmente por não existir, na maioria dos casos, uma memória física compartilhada
distribuída. Portanto, nos sistemas distribuídos, essa comunicação se dá exclusivamente por passagens de
mensagens nos agentes envolvidos por meio de protocolos e métodos próprios para conversação entre os
hosts.
O modelo cliente/servidor é a base para os sistemas de computação distribuída. Nele, um processamento
cooperativo de requisições submetidas por um cliente a um servidor que as processa retorna um resultado,
estando esses recursos de cliente e servidor espalhados sobre vários computadores.
 Cliente/servidor.
Em um modelo cliente/servidor, temos dois agentes envolvidos nessa comunicação com funções muito bem
definidas, na qual processos servidores entregam serviços para máquinas clientes. Com isso, o ambiente gera
simplicidade e eficiência em todo o processo.
Podemos afirmar, então, que:
A troca de mensagens é um processo que envia uma mensagem pela rede destinada a um ou mais
processos.
A abrangência pode ser local ou remota, e as mensagens podem ter diversos comportamentos, como
tamanho (fixo ou variável, limitado ou não), bem como ser síncronas ou assíncronas. 
Para aumentar a escalabilidade em um sistema distribuído, devemos fazer uso de algumas técnicas para
resolver problemas relacionados a distância com perdas e atrasos dos pacotes. Uma delas é evitar a espera
por respostas a requisições enviadas a dispositivos remotos, permitindo seguir executando outras tarefas
independentemente da resposta a ser recebida por quem requisitou os serviços. Com isso, é possível
trabalhar em outras tarefas e, com a chegada da resposta, o sistema pode tratar de forma coerente e concluir
uma requisição enviada anteriormente. Essa técnica é denominada comunicação assíncrona.
Muitas aplicações paralelas são candidatas nativas, assim como aplicações em lote, comumente usadas em
processamento de Big Data. Elas podem ter suas execuções, tipicamente independentes, orquestradas para
execução na qual outra tarefa fica responsável em monitorar a conclusão da comunicação.
 COMENTÁRIO
Mas nem tudo são boas notícias. Existem muitas aplicações que não podem usar a comunicação assíncrona,
principalmente aplicações interativas, quando o usuário envia uma requisição e tem que esperar até receber
uma resposta. Podemos, no entanto, resolver esse problema entregando parte da resolução para o lado
cliente executar.
COMUNICAÇÃO EM GRUPO
Coordenar sistemas distribuídos é uma das tarefas mais difíceis de se adotar devido a diversos fatores, tais
como:
Computadores podem falhar ou ficar indisponíveis por algum outro motivo.
Novas máquinas podem ser inseridas e/ou removidas do sistema.
Linhas de comunicações não são 100% confiáveis.
Eventos podem ocorrer de forma concorrente.
Em comunicações com chamadas a procedimentos remotos (RPC – Remote Procedure Calls), envolvemos
somente dois processos que permitem que programas chamem procedimentos localizados em outras
máquinas.
Quando um processo na máquina A chama um procedimento na máquina B, o transmissor A será suspenso
enquanto estiver executando o procedimento em B.

As informações são transportadas do transmissor para o receptor por parâmetros, e retornam o resultado da
execução do procedimento. Assim, nenhuma troca de mensagens será visível ao programador.
A ideia é fazer com que uma chamada de procedimento remoto pareça o mais possível com uma chamada
local, tendo que ser transparente. Sendo assim, o procedimento de chamada não precisa estar ciente de que
o procedimento enviado está em execução em uma máquina diferente, e vice-versa.
 ATENÇÃO
Esse processo, no entanto, gera um problema: ele não consegue ser executado por diversas máquinas ao
mesmo tempo. Sempre ficará restrito a duas entidades, um transmissor e um receptor.
Como podemos, então, resolver esse problema?
A resposta é simples: comunicação em grupos.
Com uma única operação, podemos enviar uma mensagem a vários destinos, permitindo que processos em
um grupo sejam tratados como uma única abstração. Esse conjunto de processos cooperam entre si para
prover um serviço, coordenando a troca de mensagens entre os membros do grupo e dos processos externos
com o grupo.
Os grupos são dinâmicos, por isso novos grupos podem ser criados e os antigos podem ser destruídos.
Um processo pode juntar-se a um grupo ou deixá-lo, e é possível ser membro de diversos grupos ao mesmo
tempo.
São necessários, portanto, mecanismos para gerenciar grupos e os processos participantes dos grupos.
Quando usamos mecanismos de comunicação em grupo, os processos tratam com coleções de outros
processos de forma abstrata.
Se um processo envia uma mensagem para um grupo de servidores, esse processo nãoprecisa se preocupar
com quantos são os servidores nem onde eles estão localizados.
Já na chamada seguinte, o número de servidores desse grupo e suas localizações podem mudar, sendo essa
alteração transparente para o processo, que envia a mensagem para o grupo.
São aplicações de comunicação em grupos:
Servidores altamente disponíveis e confiáveis.
Replicação de bancos de dados.
Conferências multimídia.
Jogos distribuídos.
Aplicações que em geral necessitem de uma alta taxa de disponibilidade, confiabilidade e tolerância a
falhas.
A comunicação pode ser implementada usando os conceitos de multicast, broadcast ou unicast:
MULTICAST
Os pacotes são enviados de uma só vez para todos os processos de um grupo.
BROADCAST
Os pacotes são enviados para todas as máquinas, e somente os processos que fazem parte do grupo não os
descartam.
UNICAST
Transmissão estilo ponto a ponto, na qual o processo tem que enviar mensagem para cada membro do grupo.
A maneira como será implementada a comunicação em grupo depende das características envolvidas na
interligação dos equipamentos. Em algumas redes, é possível criar um endereço especial no qual várias
máquinas podem escutar. Quando uma mensagem é enviada para um desses endereços, automaticamente
ela será recebida por todas as máquinas que estão escutando esse endereço. Essa técnica é chamada de
multicasting, e implementá-la é simples: basta atribuirmos um endereço de multicasting para cada grupo.
 Comunicação multicast.
Temos, ainda, redes que suportam a comunicação em broadcasting, na qual as mensagens contêm um
determinado endereço que é escutado por todas as máquinas (broadcast global) ou direcionado a uma sub-
rede específica (broadcast local).
 COMENTÁRIO
O broadcasting pode ser usado para implementar comunicação em grupo, embora não seja tão eficiente. Isso
porque todos na rede irão receber as mensagens, e cada processo terá que avaliar se aquela mensagem
recebida é para o grupo ao qual ele pertence ou não. Nesse caso, o processo terá gasto recurso de tempo e
de processamento de forma desnecessária. Mesmo assim, temos a vantagem que uma única mensagem
enviada poderá atingir a todos os equipamentos de uma determinada rede.
 Comunicação broadcast.
Quando a comunicação é um a um, a denominamos de unicasting.
 Comunicação unicast.
Além das características normais encontradas nos mecanismos de troca de mensagens − tais como
bufferização e blocagem −, no tratamento de grupos temos novas características de organização,
endereçamento e outras que serão detalhadas a seguir.
Podemos ter grupos fechados, nos quais somente os membros podem mandar mensagens para os outros
membros, que geralmente são utilizados para execução paralela.
 ATENÇÃO
Os processos de fora não podem enviar mensagens para o grupo como um todo, embora possam enviar
mensagens para membros individuais.
E podemos ter os grupos abertos, nos quais qualquer processo pode enviar uma mensagem para qualquer
grupo. Normalmente, esse tipo de mensagem é muito utilizado em servidores replicados.
 Grupos fechados x grupos abertos.
Quanto à organização, possuímos:
GRUPOS PARES OU SEMELHANTES
Todos os processos são tratados como iguais ou pares, e as decisões são tomadas coletivamente. Essa
característica está relacionada à estrutura interna do grupo. Em alguns grupos, os processos são iguais ou
semelhantes, nenhum processo é gerente e todas as decisões são feitas coletivamente.
GRUPOS HIERÁRQUICOS
Um processo é o coordenador e os demais estão subordinados a ele − uma visão mestre/escravo. Nesse
caso, quando uma requisição é feita, seja por um cliente externo ou por um dos membros do grupo, ela é
enviada ao coordenador. Este decide, então, qual dos processos do grupo é o mais indicado para executar a
solicitação.
 Grupos semelhantes x hierárquicos.
Cada uma dessas organizações tem suas próprias vantagens e desvantagens.
A vantagem é que na comunicação em grupos de semelhantes não existe um ponto único de falha. Se um
dos processos falhar, o grupo continua, apenas se torna menor.

A desvantagem é que a tomada de decisão é mais complicada. Qualquer decisão precisa envolver todos do
grupo, gerando uma demora e uma sobrecarga na comunicação e dos eventos.
No grupo hierárquico temos o oposto, já que todas as decisões são tomadas pelo coordenador, como qual
processo do grupo vai executar qual atividade.
Quando um processo termina, ele avisa ao coordenador que está pronto para outra atividade.
A perda do coordenador em um grupo hierárquico, portanto, causa uma parada nas atividades do grupo.
Nesse caso, algum algoritmo pode ser usado para eleger um processo coordenador, se o principal falhar.
 ATENÇÃO
Em um mecanismo de comunicação em grupo, é necessário um controle sobre a criação e destruição de
grupos, bem como permitir que processos se integrem a eles grupo ou os deixem.
Um enfoque é ter um servidor de grupo para o qual todas as requisições possam ser feitas. Esse servidor
pode manter um controle total de todos os seus grupos e sobre quais participantes pertencem a qual grupo,
estratégia essa concentrada no modelo centralizado. Fácil de implementar, eficiente e de rápido entendimento.
Infelizmente, ele compartilha com a maior desvantagem das técnicas centralizadas: um único ponto de falha.
Se o servidor de grupos falhar, o gerenciamento do grupo deixa de existir. Provavelmente, a maioria dos
grupos terá que ser reconstruída, interrompendo o trabalho que estava em andamento.
O método oposto é gerenciar os grupos de forma distribuída.
Em um grupo aberto, um processo de fora pode enviar uma mensagem para todos anunciando a sua
presença.

Um grupo fechado só aceita mensagem para todos de algum processo de fora se for uma solicitação para
participar do grupo.
Uma mensagem direta sem antes estar participando do grupo fechado não seria aceita. Para um processo
deixar de fazer parte de um grupo, basta enviar uma mensagem de adeus (goodbye) a todos.
Consideram-se, ainda, duas características relativas ao controle dos participantes do grupo.
Se um membro falhar, ele efetivamente deixa de pertencer ao grupo. O problema é que pode não existir um
aviso a todos os outros membros, como quando o processo deixa o grupo de forma padrão. Os demais
membros têm que descobrir por conta própria que aquele participante não está respondendo e o excluir do
grupo.
Outro ponto é o sincronismo necessário entre as mensagens sendo enviadas e a entrada ou saída de um
membro no grupo.
Uma última preocupação relacionada ao controle dos participantes do grupo é quando muitas máquinas
param de forma anormal.
 EXEMPLO
Quando ocorre um particionamento da rede, deixando o grupo inoperante. Algum protocolo se faz necessário
para reconstruir o grupo com as máquinas que restaram.
Podemos citar, ainda, outros aspectos importantes em uma comunicação em grupos, como atomicidade ou
broadcast atômico, em que uma mensagem chegará a todos os membros do grupo ou para nenhum (ou tudo
ou nada).
Quando qualquer processo enviar uma mensagem para um grupo, ele não deve preocupar-se se algum outro
membro do grupo não recebeu essa mensagem.
Com essa propriedade da atomicidade, o processo receberá uma mensagem de erro caso um ou mais
integrantes do grupo tenham tido problemas no recebimento.
Do ponto de vista de programação, tudo se passa como se fosse enviado para apenas um receptor.
Outra propriedade importante é a ordenação de mensagens. As mesmas podem chegar desordenadas, e
precisaríamos de um sistema de buffer para armazená-las e tratá-las de forma adequada.
 EXEMPLO
Por exemplo, adotar uma postura de ordenação global − que entrega todas as mensagens exatamente na
ordem na qual foram criadas − ou ordenação consistente − na qual o sistema decide qual mensagem precede
outra ‒, quando as mensagens são enviadas quase ao mesmo tempo, e entregar as mensagens nessa ordem.
Um mecanismo de comunicação em grupo precisa ter uma semântica bem definida comrelação à ordem em
que as mensagens serão entregues. A melhor garantia para isso é fazer com que as mensagens sejam
entregues na mesma ordem em que foram enviadas.
 COMENTÁRIO
Como referência de estudo do ambiente de controle de sistemas de gerenciamento de cluster focados em Big
Data, existe a ferramenta ZooKeeper, que mostra uma visão prática de uma coordenação de eventos e
comunicação efetiva em sistemas distribuídos.
CÓDIGO MÓVEL
Em sistemas distribuídos tradicionais, os componentes de aplicações são essencialmente estáticos.
O código móvel denomina um conjunto de tecnologias de linguagem e plataforma de sistemas distribuídos
que suportam a construção de programas de computador instalados em computadores servidores.
Esses programas são transferidos, sob demanda, para os computadores clientes e automaticamente
executados, da forma mais segura possível, para eles. Ou seja, executam atualizações dinâmicas fazendo
injeção de código em programas/componentes em execução, adaptando-se a condições do processo de
execução.
Um componente é instanciado em um determinado nó do ambiente e permanece neste durante todo seu ciclo
de vida.
1
Interações remotas entre componentes se dão por meio de troca de mensagens pela rede.
As tecnologias que suportam mobilidade de código permitem que trechos de código possam ser transmitidos
pela rede para serem executados em locais remotos.
2
3
A migração não é transparente aos desenvolvedores de aplicações distribuídas, mas explicitamente
manipulada por eles para atingir objetivos próprios da aplicação.
Mobilidade de código fornece flexibilidade para o gerenciamento da configuração de código em um ambiente
distribuído, e pode oferecer vantagens para a execução de atividades distribuídas. A adequação de applets
Java para a modelagem de interações com usuários finais, por exemplo, é observável pelo número de
aplicações na Internet que utilizam essa tecnologia.
Os applets são pequenos programas Java, criados pela Sun Microsystems em 1995, que são instalados em
servidores Web e referenciados por páginas HTML. Quando um browser acessa essa página HTML que
referencia o applet, ele automaticamente também transfere o código do applet e o executa.
Os applets são, nos dias atuais, os principais responsáveis pela grande disseminação do conceito e das
tecnologias de código móvel em geral.
Grande parte dos esforços para o suporte a código móvel se destina ao desenvolvimento de sistemas de
agentes móveis e de aplicações baseadas em tais sistemas. Um agente móvel (ou simplesmente agente) é
um elemento de software autocontido responsável pela execução de uma tarefa, capaz de migrar de forma
autônoma em um sistema distribuído. Um agente migra em um ambiente distribuído de um local lógico −
denominado neste conteúdo de agência − a outro.
Quando um agente migra, sua execução é suspendida na sua origem. Ele é transportado (isto é, seu código,
estado de dados, estado de execução e informação de controle) para outro componente no sistema
distribuído, no qual sua execução é retomada. Ou seja, o modelo de código móvel basicamente desloca a
carga computacional para a máquina cliente.
 COMENTÁRIO
Como percebemos até o momento, do ponto de vista da segurança, temos diversos problemas associados, e
aos poucos esse modelo de computação distribuída vem sendo tratado com muito cuidado pelo mercado
corporativo.
Como características acopladas à tecnologia de código móvel, podemos apontar o uso de uma execução
segura dentro de um sandbox, no qual o código é trazido de um servidor qualquer, denominado código
estrangeiro, e executado em um espaço de nomes restrito (o sandbox). Dessa forma, na execução de seu
código, ele terá acesso limitado à CPU, à memória e a quaisquer outros recursos computacionais gerenciados
pelo sistema operacional por meio de métodos de controle de acesso lógico.
 EXEMPLO
Podemos descrever os applets Java que, quando em execução dentro de um sandbox, apresentam diversas
restrições.
Já o modelo de código móvel usando ActiveX não utiliza o conceito de sandbox. Sua segurança de execução
é baseada no processo de autenticação da procedência do código por assinaturas digitais validadas por uma
entidade certificadora por intermédio de certificados digitais. Assim, o programa executa no ambiente
operacional do usuário sem qualquer restrição computacional.
 ATENÇÃO
Para um código ser móvel, ele deve possuir uma visão completa de abstração, de tal forma que não fique
preso a detalhes e dependências do sistema operacional e/ou ao hardware escolhido. Ou seja, deve ser
aderente em múltiplas plataformas e sistemas operacionais.
Por meio de um projeto de máquina virtual consistente, é possível se garantir a execução homogênea de um
mesmo código móvel em várias plataformas específicas sem que seja necessário se recompilar esse código.
 EXEMPLO
Programas construídos na linguagem Java são compilados para um formato de código intermediário, chamado
de bytecode. A máquina virtual Java que interpreta esses bytecodes é um microprocessador, implementado
em software. Ele interpreta um conjunto de aproximadamente 160 instruções de uma máquina virtual
especialmente projetada para executar programas Java.
A execução de programas de código móvel é feita sob demanda dos clientes. Sendo assim, não há como
prever quando um programa disponibilizado em um servidor será utilizado por quaisquer clientes.
Para que funcione adequadamente sob essas condições de imprevisibilidade, todas as etapas precisam
ocorrer da forma mais automática e rápida possível, sem que o usuário esteja envolvido em uma série de
questionamentos sobre a configuração local do programa.
As etapas são:
Transmissão do código (do servidor para o cliente).

Instalação do código (ativação da máquina virtual, carga do sandbox, verificação da consistência do código
recebido, ligação entre o código recebido e as bibliotecas locais do cliente).

Execução do código, com a criação de processos, alocação de memória para dados e instruções etc.
Com isso em mente, entregamos transparência e desempenho aceitável na transmissão, instalação e
execução do código.
ORIENTAÇÃO A EVENTOS
Um evento pode ser definido como uma mudança significativa do seu estado.
 EXEMPLO
Quando um consumidor adquire um imóvel, o estado dele se modifica de "à venda" para "vendido".
A arquitetura desse sistema pode tratar essa mudança de estado como um evento cuja ocorrência pode ser
divulgada para outros aplicativos dentro da sua arquitetura.
CONCEITOS DE SISTEMAS DE MENSAGENS
De uma perspectiva formal, o que é produzido, publicado, propagado, detectado ou consumido é uma
mensagem (geralmente assíncrona), chamada de notificação de evento, e não o próprio evento, que é a
mudança de estado que acionou a emissão da mensagem. Isso se deve às arquiteturas orientadas a eventos,
muitas vezes sendo projetadas sobre arquiteturas orientadas a mensagens, nas quais tal padrão de
comunicação requer que uma das entradas seja somente texto (a mensagem), para diferenciar como cada
comunicação deve ser tratada.
Um sistema de mensagens é um dos mecanismos mais comumente usados para troca de informações entre
aplicações.
Ao escolher seu mecanismo de integração de aplicações, é importante ter em mente os princípios
orientadores discutidos anteriormente.
No caso de bancos de dados compartilhados, por exemplo, alterações feitas por um aplicativo podem afetar
diretamente outros aplicativos que estão usando as mesmas tabelas de banco de dados. Ambas as aplicações
são fortemente acopladas.
Você pode querer evitar isso nos casos em que tenha regras adicionais a serem aplicadas antes de aceitar as
alterações no outro aplicativo.

Da mesma forma, você deve pensar sobre todos esses princípios orientadores antes de finalizar formas de
integração entre suas aplicações.
Um sistema de mensagens atua como um componente de integração entre vários aplicativos. Um sistema
orientado a eventos normalmenteconsiste em:
EMISSORES (OU AGENTES)
Os emissores têm a responsabilidade de detectar, reunir e transferir eventos. Um emissor de evento não
conhece os consumidores, nem mesmo sabe se existe ou não um consumidor e, caso exista, não sabe como
o evento será utilizado ou processado.
CONSUMIDORES (OU COLETORES)
Os coletores têm a responsabilidade de aplicar uma reação assim que um evento seja apresentado. A reação
pode ou não ser totalmente fornecida pelo próprio coletor. Por exemplo, o coletor pode ter apenas a
responsabilidade de filtrar, transformar e encaminhar o evento para outro componente ou pode fornecer uma
reação independente a tal evento.
CANAIS DE EVENTOS
Os canais de eventos são transmitidos dos emissores para consumidores. A implementação física dos canais
pode ser baseada em componentes tradicionais, como middleware orientado à mensagem ou comunicação
ponto a ponto.
Em um projeto de um sistema de integração entre aplicações, alguns fatores/princípios devem ser colocados
em mente, tais como: fracamente acoplado, definições de interfaces comuns, latência e confiabilidade. Vamos
compreender cada um deles a seguir:
FRACAMENTE ACOPLADO
A interação entre as aplicações deve garantir uma dependência mínima entre elas. Isso garante que qualquer
modificação em uma aplicação não irá afetar a outra. É diferente dos sistemas fortemente acoplados, nos
quais uma aplicação é codificada com especificações predefinidas da outra aplicação, e qualquer mudança
pode quebrar ou mudar suas funcionalidades com outras aplicações dependentes.
PADRÕES DE INTERFACES COMUNS
Deve garantir um formato de dados comum acordado para troca de mensagens entre aplicativos. Isso não
apenas ajuda a estabelecer padrões de troca de mensagens entre aplicativos, mas também garante que
algumas das melhores práticas de troca de informações possam ser aplicadas facilmente. Por exemplo, você
pode escolher usar o formato de dados Avro (muito usado em Big Data) para trocar mensagens. Isso pode ser
definido como seu padrão de interface comum para troca de informações.
LATÊNCIA
É o tempo que as mensagens levam para passar entre o remetente e o receptor. A maioria dos aplicativos
deseja atingir uma baixa latência como um requisito crítico. Mesmo em um modo de comunicação assíncrono,
a alta latência não é desejável, pois um atraso significativo no recebimento de mensagens pode causar perdas
significativas para qualquer organização.
CONFIABILIDADE
Garante que a indisponibilidade temporária dos aplicativos não afete as aplicações dependentes que precisam
trocar informações. Em geral, quando o aplicativo de origem envia uma mensagem para o aplicativo remoto,
este pode estar lento ou não disponível devido a alguma falha. A comunicação de mensagens assíncronas e
confiáveis garante que a fonte da aplicação continue o seu trabalho, e garante que o aplicativo remoto
retomará sua tarefa mais tarde.
Uma vez compreendidas essas premissas iniciais, exploraremos alguns conceitos básicos de controle de
mensageria.
FILA DE MENSAGENS
Estruturas que também são referenciadas como canais. De uma forma bem simples, podemos definir como
conectores que enviam/recebem mensagens entre as aplicações de forma oportuna e confiável.
MENSAGENS (PACOTES DE DADOS)
Uma mensagem é um pacote de dados que é transmitido por uma rede para uma fila de mensagens.
O aplicativo remetente divide os dados em pacotes de dados menores e os envolve como uma mensagem
com informações de protocolo e cabeçalho.

Em seguida, ele o envia para a fila de mensagens.

De maneira semelhante, um aplicativo receptor recebe uma mensagem e extrai os dados do invólucro para
processá-los posteriormente.
REMETENTE (PRODUTOR)
Os aplicativos do remetente ou produtor são as fontes de dados que precisam ser enviados a um determinado
destino. Eles estabelecem conexões com pontos de extremidade da fila de mensagens e enviam dados em
pacotes de mensagens menores, que seguem padrões de interface comuns.
 COMENTÁRIO
Dependendo do tipo de sistema em uso, os aplicativos remetentes podem decidir enviar dados um a um ou
em lote.
DESTINATÁRIO (CONSUMIDOR)
São os destinatários das mensagens enviadas pelo aplicativo remetente.
Eles extraem ou recebem, por meio de uma conexão persistente, dados de filas de mensagens.

Em seguida, extraem dados desses pacotes de mensagens e os usam para processamento posterior.
PROTOCOLOS DE TRANSMISSÃO DE DADOS
Determinam as regras para controle das trocas de mensagens entre aplicativos. Diferentes sistemas de filas
usam diferentes protocolos de transmissão de dados. Alguns exemplos de tais protocolos de transmissão de
dados são:
AMQP (Advance Message Queuing Protocol).
STOMP (Streaming Text Oriented Message Protocol).
MQTT (Message Queue Telemetry Protocol).
HTTP (Hypertext Transfer Protocol).
MODO DE TRANSFERÊNCIA
O modo de transferência em um sistema de mensagens pode ser entendido como a maneira pela qual os
dados são transferidos de uma aplicação de origem para a aplicação receptora.
 EXEMPLO
Modos síncrono, assíncrono e em lote.
SISTEMA DE MENSAGENS POINT-TO-POINT (PTP)
Em um modelo PTP, os produtores de mensagens são chamados de remetentes (senders) e os
consumidores, de destinatários (receivers). Eles trocam mensagens para um destino denominado fila. Os
remetentes produzem mensagens para uma fila e os destinatários consomem mensagens dessa fila.
O que distingue esse sistema é que uma mensagem pode ser consumida por apenas um único destinatário.
Pode haver vários destinatários ouvindo na fila da mesma mensagem, mas apenas um deles a receberá.
 Sistema de mensageria PTP.
 COMENTÁRIO
Note que também pode haver vários remetentes, que enviarão mensagens para a fila, mas estas serão
recebidas por apenas um destinatário.
Os remetentes podem compartilhar uma conexão ou usar conexões diferentes, mas todos podem acessar a
mesma fila. Assim, mais de um destinatário pode consumir mensagens de uma fila, mas cada mensagem
pode ser consumida por apenas um destinatário.
Observe na imagem a seguir, que as mensagens 1 e 2 são consumidas por diferentes destinatários. Como no
caso dos remetentes, os destinatários também podem compartilhar uma conexão ou usar conexões
diferentes, assim como todos podem acessar a mesma fila.
 Sistema de mensageria PTP múltiplos remetentes/destinatários.
 ATENÇÃO
Remetentes e destinatários não têm dependência de tempo; o destinatário pode consumir uma mensagem,
independentemente de estar em execução quando o remetente produziu e enviou a mensagem.
As mensagens são colocadas em uma fila na ordem em que são produzidas, mas a ordem em que são
consumidas depende de fatores como data de vencimento ou prioridade da mesma, se um seletor é usado no
consumo de mensagens e a taxa relativa de processamento da mensagem consumidora. Os remetentes e
destinatários podem ser adicionados e/ou excluídos dinamicamente no tempo de execução, permitindo que o
sistema se expanda ou contraia conforme necessário.
SISTEMA DE MENSAGENS PUBLISHER/SUBSCRIBE (PUB/SUB)
Neste tipo de modelo, um assinante (subscribe) registra seu interesse em um determinado tópico ou evento e
é posteriormente notificado sobre este de forma assíncrona. Os assinantes são subsequentemente notificados
de qualquer evento gerado por um editor (publisher) que corresponda ao seu interesse registrado. Esses
eventos são gerados pelos editores. 
Um modelo de mensagem Pub/Sub é usado quando você precisa transmitir um evento ou mensagem para
muitos consumidores. Ou seja, nesse modelo todos os assinantes que estiverem ouvindo o tópico receberão a
mensagem. Além disso, estas podem ser retidas no tópico até que sejam entregues aos assinantes ativos. O
ponto importante aqui é que vários assinantes podem consumir a mensagem.
 Sistema de mensageria Pub/Sub.
Podemos resumir o fluxo do sistema Pub/Sub da seguinte forma:
As mensagens são compartilhadas por meio de um canal chamado tópico. Um tópico é um local centralizadoonde os editores (publishers) podem publicar e os assinantes (subscribers) podem consumir mensagens.
Cada mensagem é entregue a um ou mais consumidores, chamados assinantes.
O editor geralmente não sabe e não tem conhecimento de quais assinantes estão recebendo as mensagens
de tópico. Estas são enviadas aos assinantes/consumidores sem a necessidade de terem sido solicitadas.
As mensagens entregues a um tópico são enviadas automaticamente a todos os consumidores qualificados.
Não há acoplamento dos editores com os assinantes. Ambos, assinantes e editores, podem ser adicionados
dinamicamente em tempo de execução, o que permite ao sistema aumentar ou diminuir a complexidade ao
longo do tempo.
Todo consumidor que assina um tópico recebe sua própria cópia das mensagens publicadas nesse tópico.
Uma única mensagem produzida por um editor pode ser copiada e distribuída para centenas ou mesmo
milhares de assinantes.
 Representação gráfica do modelo Pub/Sub.
Qualquer situação na qual seja necessário notificar vários consumidores de um evento é um bom uso do
modelo Pub/Sub.
 EXEMPLO
Você deseja enviar uma notificação a um tópico sempre que ocorrer uma exceção em seu aplicativo ou
componente do sistema. Você pode não saber como essas informações serão usadas ou que tipos de
componentes as usarão. O editor/transmissor não se importará ou precisará se preocupar com a forma como
as informações serão usadas, ele simplesmente publica em um tópico. Essa é a magia de um sistema
fracamente acoplado.
PROTOCOLO DE ENFILEIRAMENTO DE MENSAGENS
AVANÇADO (AQMP)
Como abordado no início deste tópico, existem diferentes tipos de protocolos de transmissão que definem
como os dados irão fluir entre transmissores, receptores e filas de mensagens. Vamos, então, abordar um
protocolo que se faz presente em produtos concentrados em Big Data, como o Apache Kafka: o AQMP
(Advanced Queue Messaging Protocol).
AQMP é um protocolo aberto para enfileiramento de mensagens assíncronas que se desenvolveu e
amadureceu ao longo de vários anos. AQMP fornece um conjunto completo de funcionalidades de mensagens
que pode ser usado para suportar cenários de mensagens bem avançados.
Conforme ilustrado na imagem a seguir, existem três componentes principais em qualquer sistema de
mensagens baseado em AQMP:
 Arquitetura do AQMP.
Nessa arquitetura, os produtores enviam mensagens aos brokers (corretores), que, por sua vez, as entregam
aos consumidores.
 COMENTÁRIO
Cada broker possui um componente chamado exchange (intercâmbio) que é responsável por rotear as
mensagens dos produtores para as filas de mensagens apropriadas.
Cada componente pode ser multiplicado, aumentando, assim, a escalabilidade da solução. Produtores e
consumidores comunicam-se entre si por meio de fila de mensagens associadas aos brokers. O protocolo
AQMP provê confiabilidade e garantia na entrega de mensagens ordenadas.
Vimos aqui a importância dos sistemas de mensageria em sistemas distribuídos, que, em nossa tecnologia
contemporânea, têm sido muito usados, principalmente no gerenciamento de mensagens de produtos de Big
Data.
OS PARADIGMAS DE COMUNICAÇÃO EM
COMPUTAÇÃO DISTRIBUÍDA
Para finalizar este módulo, assista ao vídeo a seguir. Nele, abordamos os paradigmas para comunicação em
computação distribuída.
VERIFICANDO O APRENDIZADO
MÓDULO 2
 Identificar os modelos utilizados para coordenação de tempo e para garantia da transparência de
localização
NOMEAÇÃO
Nomes desempenham papel importante em todos os sistemas de computação, e são obviamente
fundamentais em sistemas distribuídos, sendo amplamente usados para, por exemplo, compartilhar recursos e
identificar entidades.
 ATENÇÃO
Nomes simples são bons para as máquinas, mas não são convenientes para seres humanos. Então, para
resolver isso, é necessário implementar um sistema de resolução de nomes por meio de uma nomeação
estruturada.
Em um sistema distribuído, essa implementação costuma ser partilhada por várias máquinas, e a forma como
a distribuição é feita influencia diretamente na escalabilidade e eficiência do sistema. Os nomes responsáveis
pela identificação unívoca de entidades e localizações desempenham um importante papel nos sistemas
computacionais.
Chamamos de entidade qualquer coisa, como discos, hosts, impressoras, arquivos, dentre outros. Todos são
acessados por meio de um ponto, basicamente um endereço associado − como um servidor e seu endereço
IP −, e saber o endereço IP não torna o processo amigável. Essa entidade pode oferecer mais de um ponto de
acesso, e o mesmo pode mudar ao longo do tempo.
 COMENTÁRIO
Os nomes são organizados no conceito de espaço de nomes. Um nome independente de sua localização
significa que o nome da entidade é independente do seu endereço. Ou seja, nada poderá ser referenciado
sobre o endereço da entidade associada.
Vamos averiguar os seguintes exemplos a seguir:
http://www.xyz.org/index.html
http://www.xyz.br/index.html
Podemos concluir que os dois exemplos são independentes de localização, mas, como podemos observar, o
primeiro não referencia de forma consistente a localização da entidade.
Caso um sistema distribuído seja reorganizado e servidores mudem de endereço, é importante que seja
possível continuar a acessar o serviço de forma totalmente transparente. E, para isso, devemos adotar um
nome designado para ser usado por seres humanos, constituídos por cadeias de caracteres de fácil
entendimento, denominado “nome amigável”.
Um identificador é um nome que reconhece uma entidade e possui as seguintes propriedades:
1
Um identificador referencia, no máximo, uma entidade.
Cada entidade é referenciada por, no máximo, um identificador.
2
3
Um identificador sempre referencia a mesma entidade, isto é, nunca é reutilizado.
O uso de identificadores permite que entidades sejam referenciadas sem ambiguidade.
4
.
Podemos citar como exemplos de identificadores:
ISBN de livros.
Endereço MAC ethernet.
Matrícula de um funcionário.
Código de identificação de produtos.
Um sistema de nomeação mantém uma vinculação nome-endereço, que, em sua forma mais simples, é
apenas uma tabela de pares (nome, endereço).
Em sistemas distribuídos de grande escala, é necessário usar tabelas descentralizadas, assim como funciona
o Domain Name Service (serviço de nomes de domínio), no qual um nome é decomposto em várias partes e a
resolução é feita por meio de consultas recursivas das partes.
Assim, o servidor referenciado como nome totalmente qualificado xyz.com.br tem a seguinte árvore, com
seus respectivos serviços de nome associados e distribuídos:
. (ns) 
br (ns) 
com (ns) 
xyz (ns) 
Em uma nomeação hierárquica estruturada como o serviço de resolução de nomes (DNS), podemos resolver
os nomes de duas maneiras.
FORMA ITERATIVA
O servidor responderá somente com o que sabe: o nome do próximo servidor a ser buscado, e o cliente
procura iterativamente os outros servidores.
FORMA RECURSIVA
O servidor passa o resultado para o próximo servidor que encontrar, e ao cliente restará somente receber a
mensagem de endereço de nome encontrado ou não encontrado.
A principal desvantagem no método recursivo é uma maior exigência de desempenho aos servidores de
nomes, pois estes precisam manipular a resolução completa de um caminho.
No entanto, tem duas grandes vantagens:
Armazenar temporariamente os resultados intermediários, diminuindo o custo de comunicação.
Cada servidor de nomes no caminho aprende gradativamente o endereço de cada servidor de nomes de
nível mais baixo.
O DNS é, portanto, o sistema de resolução de nomes da Internet e base para grande parte dos sistemas
distribuídos.
De forma hierárquica, ele gera uma base de dados distribuída que permite localizar rapidamente qualquer
entidade mapeada para a Internet ou dentro de uma rede local em uma visão de intranet.
Sua implementação é feita no que chamamos de zonas, que não se sobrepõem. Cada zona é construída em
um servidor de nomes, que possuiautoridade por um pedaço do espaço de nome por ele gerenciado e pode
ser replicado, garantindo maior disponibilidade nos serviços prestados.
javascript:void(0)
javascript:void(0)
 Resolução de nomes.
NOMEAÇÃO EM ATRIBUTO
Nesta abordagem, são fornecidas descrições a partir de termos de pares (atributo, valor), conhecidos como
serviços de diretórios. Com estes, as entidades têm um conjunto de atributos associados que podem ser
usados para procurá-las.
Consultar valores em um sistema de nomeação baseado em atributos requer uma busca exaustiva nos
descritores, o que pode acarretar sérios problemas de desempenho e escalabilidade. Como exemplo prático,
temos implementações hierárquicas − como o LDAP (Protocolo Leve de Acesso a Diretório) − e
implementações descentralizadas − as tabelas Hash distribuídas (DHT).
A abordagem de implementações hierárquicas combina nomeação estruturada com nomeação baseada em
atributos amplamente adotada, como no Active Directory da Microsoft ou no ambiente de código aberto com o
OpenLDAP.
O serviço de diretório LDAP é organizado na forma de registros, conhecidos como entrada de diretório. Nela,
cada registro é composto por um conjunto de pares (atributo, valor), nos quais cada atributo possui um tipo
associado e os valores podem ter valor único ou serem multivalorados.
 EXEMPLO
Os atributos: C = Country (País), O = Organization (Organização), OU = Organization Unit (Unidade
Organizacional).
Populamos, então, com os atributos:
uid=professor, C=br, O=xyz, OU=centro.
A implementação de um serviço de diretório LDAP é semelhante à implementação do serviço de nomeação do
DNS. Diretórios de grande escala particionam as árvores de diretório, distribuindo-as em vários servidores,
equivalentes às zonas em DNS, mas com a diferença de suportar um número bem maior de valores para
consultas, predefinidas em uma estrutura denominada esquemas.
DISTRIBUTED HASH TABLES (DHT)
As tabelas de Hash distribuídos são uma classe de sistemas distribuídos descentralizados que provém um
serviço de busca parecida com a de uma tabela de Hash: pares (atributo, valor) são armazenados no DHT, e
qualquer nó pode recuperar o valor associado a cada atributo.
A responsabilidade por manter o mapa dos atributos até os valores é distribuída entre os nós de modo que
uma mudança no grupo de nós cause uma interrupção mínima. Isso permite que o DHT funcione para uma
grande escala de nós, com estes chegando, saindo e falhando continuamente, possibilitando uma busca
eficaz, em vez de executar uma busca excessiva por toda a extensão do espaço de atributos.
 COMENTÁRIO
Um problema do DHT, porém, é que ele só realiza buscas pelo nome exato do arquivo, no lugar de usar
palavras-chave. Essa solução é muito utilizada para nomeação em sistemas peer-to-peer.
Nesse ambiente, existe uma descentralização, não há uma entidade de coordenação central. O ambiente é
altamente escalável, estando preparado para funcionar corretamente tanto com milhares de nós como para
milhões de nós sem que haja uma queda na qualidade do serviço oferecido. É totalmente tolerante a falhas, e
a entrada, a saída e a falha de peers não devem abalar o sistema, mesmo que essas ocorram
frequentemente.
Para prover essas características, uma técnica usada é fazer com que cada nó coordene apenas uma
quantidade pequena de outros nós, quando comparada com a quantidade total na DHT.
Dessa forma, é necessário realizar somente uma quantidade limitada de trabalho para cada mudança.
São exemplos dessa estrutura: BitTorrent, eMule, YaCy, dentre muitos outros.
NOMEAÇÃO
No vídeo a seguir, apresentamos um resumo do módulo explicando sobre os sistemas de nomeação e suas
técnicas.
VERIFICANDO O APRENDIZADO
MÓDULO 3
 Analisar os conceitos de consistência e replicação nos sistemas distribuídos
SINCRONIZAÇÃO DE RELÓGIOS
A sincronização entre processos locais é a base de entendimento para a comunicação entre processos em
sistemas distribuídos, de tal forma que devem responder a simples perguntas:
Como as regiões críticas são implementadas em um sistema distribuído?
Como esses recursos são alocados?
Como é gerenciado o acesso concorrente?
Como sabemos que os eventos estão de acordo com uma linha do tempo?
Em um sistema local, as regiões críticas, a exclusão mútua e demais problemas de sincronização são
resolvidos, na maioria das vezes, por métodos de semáforos e monitores. Obviamente, porém, estes não são
recomendados para serem usados em sistemas distribuídos, porque eles “esperam” a existência de uma
memória compartilhada.
Em um sistema centralizado, o tempo não é ambíguo, então, quando um determinado processo consulta a
hora, ele faz uma chamada ao sistema, que responde.
 EXEMPLO
Se um processo A perguntar a hora e, um pouco mais tarde, um processo B fizer o mesmo, o valor obtido por
B será maior do que o obtido por A.
Entretanto, em um sistema distribuído, conseguir acordo nos horários não é trivial.
RELÓGIOS LÓGICOS
Os hosts possuem circuitos internos para gerenciar um relógio físico interno, composto por um cristal de
quartzo que, sob pressão, oscila em uma frequência definida, gerando o correto batimento para o
funcionamento do ambiente.
 COMENTÁRIO
Todos os processos da máquina utilizam esse mesmo relógio, mas, com diversos servidores espalhados, é
praticamente impossível manter esses cristais funcionando na mesma frequência.
Esses relógios podem ficar defasados uns com os outros, mas como sincronizá-los para que estejam
de acordo com o tempo real?
Para isso, Cristian (1989) propôs usar um modelo cliente/servidor no qual clientes consultam a hora em um
servidor de tempo de precisão. Em sistemas distribuídos, não se pode determinar com precisão o delay
(atraso) de transmissão da rede.
 EXEMPLO
Um cliente desejando sincronizar−se com um servidor envia−lhe uma requisição, e este responde devolvendo
o tempo atual de seu clock. O problema é que, quando o cliente recebe de volta a mensagem, o tempo
retornado já está desatualizado.
É claro que a latência de uma Wan poderá fornecer uma hora desatualizada. Criou-se, então, o protocolo de
tempo de rede (NTP – Network Time Protocol), que se ajusta em um modelo de pares de servidores no qual
um consulta o outro para entregar uma maior precisão de relógio. Normalmente, um relógio de referência
atômico, stratum 0, entregará essa precisão.
COMO FUNCIONA O PROTOCOLO NTP?
O servidor NTP consulta seus pares de tempo em tempo, perguntando a hora que cada um está registrando,
e, com base nos valores retornados, calcula um horário médio e informa para todos os participantes adiantar
ou atrasar seus relógios.
Lamport (1978) mostrou que, embora a sincronização de relógios seja possível, não precisa ser absoluta, pois,
se dois processos não interagem entre si, seus relógios não precisam ser sincronizados. Assim, não é
necessário que todos os processos concordem com a hora exata, mas com a ordem em que os eventos
ocorrem.
SOLUÇÕES DE PROBLEMAS DE SINCRONIZAÇÃO
EM SISTEMAS DISTRIBUÍDOS
ALGORITMOS CENTRALIZADOS
O caminho mais rápido para atingir exclusão mútua em sistemas distribuídos é similar ao feito em sistema
com um único processador.
Um processo é eleito como o coordenador (por exemplo, um rodando na máquina com o maior endereço da
rede ou escolhido por algum algoritmo de eleição).
Sempre que um processo quiser entrar na região crítica, ele envia uma requisição de mensagem para o
coordenador, declarando qual região ele quer entrar e requisitando permissão.
Se nenhum outro processo estiver atualmente naquela região crítica, o coordenador envia uma resposta de
volta, dando permissão.
Quando a resposta chega, o processo de requisição entra na região crítica.
Quando o processo 1 sai dessa região, ele envia uma mensagem para o coordenador, liberando o acesso
exclusivo.
O coordenador toma as requisições da fila e envia ao processo uma mensagem de permissão.
Se o processo ainda estiver bloqueado (por exemplo, é a primeiramensagem para ele), ele é desbloqueado e
entra na região crítica.
Esse algoritmo garante exclusão mútua: o coordenador somente deixa um processo, por tempo, entrar na
região crítica. É também justo, já que as concessões às requisições são feitas na ordem na qual são
recebidas. Nenhum processo espera para sempre. Além disso, o esquema é fácil de implementar, e requer
somente três mensagens para uso da região crítica (requisição, concessão, liberação). Pode também ser
usado para alocação mais geral dos recursos, em vez de somente gerenciar regiões críticas.
 COMENTÁRIO
A abordagem centralizada também possui problemas. O coordenador é um único ponto de falha, logo, se
quebrar, todo o sistema pode parar. Se os processos normalmente bloquearem depois de uma requisição, eles
não poderão distinguir um coordenador "morto" de uma "permissão negada", já que em ambos nenhuma
mensagem retorna. Em adição, em sistemas grandes, um único coordenador pode tornar um gargalo da
performance.
ALGORITMOS DISTRIBUÍDOS
Como ter um único ponto de falha é um problema, os pesquisadores têm estudado alguns algoritmos para
garantir a exclusão mútua distribuída. O algoritmo de Ricart e Agrawala requer que haja uma ordem total de
todos os eventos no sistema. Ou seja, para qualquer par de eventos, como mensagens, não deve haver
ambiguidade sobre qual é o primeiro.
O algoritmo de Lamport, apresentado anteriormente, é um modo de alcançar essa ordenação, e pode ser
usado para fornecer um timestamp para a exclusão mútua distribuída.
Quando um processo quer entrar na região crítica, ele constrói uma mensagem contendo o nome da região de
interesse, o número do processo e o tempo corrente.
Envia, então, a mensagem para todos os outros processos, conceitualmente incluindo-o também.
Um grupo confiável de comunicação, se disponível, pode substituir as mensagens individuais (multicast em
vez de unicast).
Quando um processo recebe uma requisição de outro, a ação que se executa depende do seu estado com
relação à região crítica nomeada na mensagem.
Três casos devem ser distinguidos:
Se o receptor não é a região crítica e não quer entrar nesta, ele retorna um OK (ACK) para o
transmissor.
Se o receptor estiver na região crítica, ele não responderá. Simplesmente enfileira a requisição.
Se o receptor quiser entrar na região crítica, mas ainda não o fez, ele compara o timestamp da
mensagem com o conteúdo da mensagem que enviou para todos. A menor ganha. Se a mensagem que
chegar tiver menor prioridade, o receptor envia uma mensagem de OK. Se sua própria mensagem tiver
um timestamp inferior, o receptor enfileira a mensagem e não enviará nada.
Depois de enviar as requisições pedindo a devida autorização para entrar na região crítica, um processo vai
esperar até que todos tenham dado as devidas permissões. Assim que todas as permissões cheguem, ele
poderá entrar na região crítica. Quando sair da região crítica, enviará um OK para todos os processos da sua
fila e os deletará de sua fila. Se não houver um conflito, o mesmo funcionará como esperado.
ALGORITMO EM ANEL
Veremos, agora, uma abordagem totalmente diferente para obter a exclusão mútua em um sistema distribuído.
Um anel circular é construído, e cada processo estará associado a uma posição no anel.
Essas posições podem ser alocadas em uma ordem numérica dos endereços da rede ou de qualquer outra
forma.
Não importa qual é a ordem, importa somente que cada processo saiba que é o próximo dentro do anel.
Quando o anel é inicializado, o processo recebe um token, que circula pelo anel. Quando um processo adquire
o token do processo vizinho, é feita uma verificação para entrar na região crítica. Se precisar, o processo entra
na região crítica, efetua o trabalho a ser feito e libera a região. Depois de sair, ele passa o token para o
controle do anel. Não é permitido entrar em uma segunda região crítica usando o mesmo token.
Se o processo recebe o token de seu vizinho e não está interessado em entrar na região crítica, ele somente o
passa adiante. Como consequência, quando nenhum processo quer entrar em nenhuma região crítica, o token
somente circula em alta velocidade dentro do anel. Desse modo, somente um processo que possuir o token
poderá estar na região crítica.
Visto que o token circula pelos processos em uma ordem bem definida, um starvation não poderá ocorrer.
Uma vez que um processo decide entrar na região crítica, no pior caso, ele terá que esperar que todos os
outros processos entrem na região crítica e liberem−na.
 COMENTÁRIO
Como grande desvantagem, se o token for perdido, o mesmo deverá ser gerado novamente. Mas detectar
essa perda é difícil, visto que o tempo entre sucessivas aparições do token da rede não é limitado. O fato de
ele não ter sido avistado por uma hora não significa que ele foi perdido; alguém pode ainda estar utilizando-o.
O algoritmo também tem problemas no caso de uma falha do processo, mas a sua recuperação é mais fácil do
que nas outras situações. Um processo, ao passar o token para seu vizinho, pode perceber que ele está fora
do ar. Nesse ponto, o processo fora do ar poderá ser removido do grupo, e o que possui o token enviará para
o próximo processo, no anel. Para que isso aconteça, se faz necessário que todos mantenham a configuração
atual do anel.
ALGORITMO DE DETECÇÃO DE DEADLOCKS DISTRIBUÍDOS
Um deadlock (impasse) é causado por uma situação na qual um conjunto de processos estará bloqueado
permanentemente. Isto é, não conseguem prosseguir com sua execução, esperando um evento externo
promovido por outro processo para sair da situação de bloqueio permanente.
Um deadlock ocorre devido aos processos de alocação formarem um ciclo, no qual vários processos tentam
alocar recursos para realizar suas tarefas, mas foram bloqueados por outros, que não liberam os seus para
deixar que os outros sigam suas execuções. Assim, não terão todos os recursos disponíveis por não terem
terminado suas tarefas.
Algumas estratégias são adotadas para o tratamento de deadlock, evitando a ocorrência por meio de uma
alocação de cuidados dos recursos, prevenindo de forma estática que os deadlocks não ocorram e permitindo
que o deadlock ocorra, entretanto, que seja possível detectar e tentar se recuperar da situação. Podemos usar
um algoritmo centralizado, que examina o estado do sistema para determinar se existe um deadlock e, nesse
caso, executa uma ação corretiva.
Em sistemas distribuídos, coletar e colecionar essas informações sobre necessidade/oferta de recursos é
extremamente difícil e complicado de gerenciar. Mas é possível manter essas informações sobre alocação de
recursos, criando um grafo de alocação de recursos e procurando ciclos nesse grafo.
 ATENÇÃO
Caso o coordenador detecte um ciclo, ele matará um dos processos, acabando com o deadlock.
ALGORITMO DE ELEIÇÃO
Em sistemas distribuídos, diversos algoritmos necessitam que um processo exerça uma função especial,
como coordenador, inicializador ou sequenciador. São exemplos:
Coordenador de exclusão mútua com controle centralizado.
Coordenador para detecção de deadlock distribuído.
Sequenciador de eventos para ordenação consistente centralizada.
A falha do coordenador compromete o serviço para vários processos, portanto, um novo coordenador deve
assumir. Para isso, fazemos uma eleição. O objetivo é eleger um processo, entre os ativos, para
desempenhar função especial. Para tal, existem algoritmos especiais para escolha de um processo que
assumirá a posição de coordenador. Podemos citar os algoritmos bully (valentão) e o ring (anel).
Para esses algoritmos, assume-se as seguintes premissas:
Todo o processo no sistema tem prioridade única.
Quando a eleição acontece, o processo com maior prioridade entre os processos ativos é eleito como
coordenador.
Na recuperação (volta à atividade), um processo falho pode tomar ações para juntar-se ao grupo de processos
ativos.
ALGORITMO DE BULLY
Quando um processo nota que o coordenador não responde,uma eleição deverá ser iniciada.
1
Esse processo P envia uma mensagem de eleição para todos os processos com número maior que o seu.
Se não houver nenhuma resposta, P ganha a eleição e se torna o coordenador.
2
3
Se alguém responde, a tarefa de P estará finalizada, e a eleição poderá continuar a partir dos processos com
maior número.
Quando a eleição finaliza, o vencedor enviará uma mensagem para todos os processos informando que será o
novo coordenador.
ALGORITMO DE RING (ANEL)
Nesse algoritmo, os processos podem ser fisicamente ou logicamente ordenados. Eles sabem que são seus
sucessores e, quando um processo nota que o coordenador não responde, constrói uma mensagem de
eleição, inclui seu número e envia para seu sucessor. A mensagem circula pelo anel, e cada processo inclui
seu número iniciador da eleição, recebe a mensagem de volta e determina quem é o coordenador baseado
em qual processo possui o maior número. Após isso, envia a mensagem coordenador pelo anel.
SINCRONIZAÇÃO EM SISTEMAS DISTRIBUÍDOS
Os algoritmos para sincronização em sistemas distribuídos são apresentados no vídeo a seguir.
REPLICAÇÃO
Em nosso dia a dia, o dado, que é considerado o novo petróleo por muitos especialistas, é um ativo
fundamental para tomadas de decisões de empresas e instituições. Portanto, ter os dados replicados é de
suma importância, pois garante suas duas razões principais: confiabilidade e desempenho. Além disso, de
forma embutida, garante a disponibilidade desses dados.
Seguindo o conceito da disponibilidade, conseguimos manter os sistemas operacionais mesmo na queda de
uma das réplicas, pois pode-se redirecionar o acesso a qualquer uma das suas cópias disponíveis, além de
garantir uma melhor proteção contra dados corrompidos. O desempenho também pode ser alcançado, uma
vez que é possível ter várias cópias espalhadas, e um redirecionamento de acesso pode permitir que a carga
de trabalho seja balanceada entre suas várias réplicas.
 COMENTÁRIO
Ter muitas cópias pode levar a problemas de consistência. Sempre que houver uma atualização, será
necessário modificar todas as demais cópias para garantir a consistência dos dados, sendo esse o maior
problema em projetos que envolvam esses conceitos.
Usar uma memória local temporária para os dados acelera o desempenho e entrega um melhor “sentimento”
de velocidade aos seus utilizadores, mas gera sempre uma informação desatualizada. Chegamos novamente,
então, ao impasse desempenho versus escalabilidade, em que manter várias cópias pode significar problemas
de escalabilidade.
Assim, como um conjunto de cópias consistentes são sempre iguais, uma operação de leitura sempre
retornará o mesmo valor. Mas, em uma operação de escrita, a atualização será propagada para todas as
cópias antes que ocorra outra operação subsequente, independentemente de em qual réplica for realizada a
operação.
Chamamos de replicação síncrona ou consistência estrita a existência de uma atualização realizada de
forma atômica. No entanto, teremos dificuldade em garantir que todas as réplicas estejam sincronizadas
globalmente. Isso porque implementar métodos síncronos pode ser muito caro, na visão do desempenho
distribuído, para as aplicações sensíveis a latência.
 COMENTÁRIO
Em muitos casos, então, a melhor solução poderá ser o relaxamento das restrições de consistência
estrita, ganhando maior desempenho em situações nas quais a estratégia a ser seguida seria que as cópias
nem sempre seriam iguais em todos os lugares. Dessa forma, as aplicações deveriam estar preparadas para
receber informações um pouco desatualizadas.
Atualmente, cada vez mais vemos soluções de banco de dados NoSQL, bancos de dados distribuídos e
replicados de alta escala — nos quais estão sendo aplicados os conceitos de consistência eventual, pois nem
sempre podemos fazer uso de uma consistência estrita (síncrona) —, que toleram um alto grau de
inconsistência. Isso porque existem situações que executam muito mais operações de leitura do que de
escrita.
Portanto, a questão aqui é a velocidade com que as atualizações devem ser disponibilizadas para os
processos que realizam somente leitura. Se nenhuma atualização ocorrer por um tempo bastante longo, todas
as réplicas ficarão gradativamente consistentes. Ou seja, na ausência de atualizações, todas as réplicas
convergem em direção a cópias idênticas umas das outras, e temos a garantia de que as atualizações serão
propagadas para todas as réplicas no tempo que for programado para tal.
VERIFICANDO O APRENDIZADO
MÓDULO 4
 Identificar as principais interfaces de desenvolvimento de códigos para sistemas paralelos e
distribuídos
INTERFACES DE PROGRAMAÇÃO DISTRIBUÍDA
Com a explosão da Internet, as aplicações tiveram que se adaptar a uma nova realidade de comunicação.
Surgiu a necessidade de que elas parassem de executar em um ambiente fechado e proporcionassem uma
distribuição de conteúdo entre elas mesmas. Para proporcionar que essa distribuição ou comunicação ocorra
entre as aplicações, estas devem ser compostas em camadas, ou seja, devem se tornar aplicações
distribuídas.
 COMENTÁRIO
Criar uma aplicação distribuída é uma tarefa muito importante nos dias atuais. Isso porque, com a facilidade
de acesso promovida com a difusão da Internet, as aplicações precisam, além de se comunicarem entre si, ter
a maior disponibilidade e confiabilidade possível.
Existem APIs que funcionam em sistemas operacionais para facilitar as tarefas de comunicação e de
processamento, portanto, não precisam da utilização da Internet. Neste módulo, exploraremos algumas
dessas APIs, tais como sockets, MPI, RPC e barramento, com alguns exemplos de implementação em
linguagem C.
PROGRAMAÇÃO EM SOCKETS
O termo socket refere-se à Interface de Programação de Aplicações (API) implementada pelo grupo de
distribuição de software UNIX da Universidade de Berkeley (BSD). A interface fornecida pelo socket permite
que os processos acessem serviços de rede. A troca de mensagens é feita entre dois processos, usando
vários mecanismos de transporte.
A comunicação via sockets deve ser realizada sempre dentro de um domínio, que é caracterizado por uma
abstração feita para designar os serviços de rede, uma estrutura de endereçamento comum e protocolos entre
dois pontos de comunicação em uma rede.
Dessa forma, processos que são executados em sistemas diferentes podem se comunicar por meio dos
sockets somente se utilizarem o mesmo esquema de endereçamento. Em TCPI/IP, os sockets UDP e TCP são
a interface provida pelos respectivos protocolos na interface da camada de transporte.
 Estrutura de comunicação em sockets.
Como vimos, os sockets foram criados na forma de uma API que possibilita aplicações/processos se
comunicarem, mas quais são essas interfaces que permitem essas comunicações? A seguir, listamos algumas
das principais funções utilizadas ao criar um programa utilizando sockets.
getaddrinfo() Traduz nomes para endereços
socket() Cria um socket e retorna o descritor de arquivo
bind() Associa o socket a um endereço socket e a uma porta
connect() Tenta estabelecer uma conexão com um socket
listen() Coloca o socket para aguardar conexões
accept() Aceita uma nova conexão e cria um socket
send() Caso conectado, transmite mensagens ao socket
recv() Recebe as mensagens por meio do socket
close() Desaloca o descritor de arquivo
shutdown() Desabilita a comunicação do socket
Quando se programa utilizando sockets, uma arquitetura muito empregada é a cliente/servidor (client/server).
Para isso, temos que implementar um programa cliente e um programa servidor. Ambos fazem uso da mesma
API de sockets.
Os sockets UDP são basicamente canais não confiáveis, pois:
Não garantem entrega dos datagramas;
Podem entregar datagramas duplicados;
Não garantem ordem de entrega dos datagramas;
Não têm estado de conexão (escuta, estabelecida).
Para programar em sockets UDP, utilizamos as seguintes funções:
Para criar o socket.
DatagramSockets = new DatagramSocket(6789);
Para receber um datagrama.
receive(req)
Para enviar um datagrama.
send(resp);
Para fechar um socket.
close();
Para montar um datagrama para receber mensagem.
new DatagramPacket(buffer, buffer.length);
Para montar um datagrama para ser enviado.
new DatagramPacket(msg, msg.length, inet, porta);
Buffer e msg são byte[ ].
Como podemos observar, nesse modelo o send não é bloqueante, mas o receive é, a menos que possamos
especificar um timeout. Em uma visão multithreads, teremos um thread principal que receberá as requisições
e outras threads servindo aos respectivos clientes.
Dentro da camada de transporte da pilha TCP/IP, temos o protocolo TCP, que implementa uma comunicação
confiável.
Na visão do desenvolvedor, é visto como um fluxo contínuo (stream), e seus dados são tratados como
segmentos. Existe total garantia de entrega, ordenamento das mensagens e não ocorrência de duplicação.
Estabelece uma conexão e, portanto, controla o estado da conexão (estabelece, escuta e fecha sessões).
Possui, ainda, outras características integradas, como sessão ponto a ponto (um transmissor: um receptor), ou
seja, sockets conectados e controle de congestionamento e de fluxo, para que o transmissor não
sobrecarregue o receptor.
Ainda dentro desse contexto, um stream é tratado como uma sequência de bytes transmitida e recebida
continuamente por um processo. O TCP preocupa-se em segmentar o stream e, se necessário, entregar os
segmentos à aplicação na ordem correta. Do ponto de vista do desenvolvedor de sockets TCP, basta gravar
os dados em um buffer de saída para que sejam enviados e ler os dados de chegada em um buffer de
entrada.
Para programar em sockets TCP, utilizamos as seguintes funções:
Servidor cria socket de escuta em uma porta (Ex.: 6789).
ServerSocket ss = new ServerSocket(6789);
Servidor aceita uma conexão e cria um novo socket para atendê-la.
Socket a = ss.accept();
Cliente cria socket de conexão.
Socket s = new Socket(“localhost”, 6789);
Fecha o socket.
close();
LADO CLIENTE
O programa cliente primeiro cria um socket por meio da função "socket()". Em seguida, conecta-se ao servidor
por meio da função "connect()" e inicia um loop (laço), que fica fazendo "send()" (envio) e "recv()"
(recebimento) com as mensagens específicas da aplicação. É no par send/recv que temos a comunicação
lógica. Quando alguma mensagem da aplicação diz que é o momento de terminar a conexão, o programa
chama a função "close()" para finalizar o socket.
LADO SERVIDOR
O programa servidor também utiliza a mesma API de sockets. Ou seja, inicialmente ele também cria um
socket. No entanto, diferentemente do cliente, o servidor precisa fazer um "bind()", que associa o socket a
uma porta do sistema operacional, e depois utilizar o "listen()" para escutar novas conexões de clientes nessa
porta.
Quando um novo cliente faz uma nova conexão, a chamada "accept()" é utilizada para começar a se
comunicar. Da mesma forma que no cliente, o servidor fica em um loop, recebendo e enviando mensagens por
meio do par de funções "send()" e "recv()".
Quando a comunicação com o cliente termina, o servidor volta a aguardar novas conexões de clientes.
 Estrutura de sockets cliente/servidor.
EXEMPLO DE IMPLEMENTAÇÃO DE UM CÓDIGO PARA O LADO
CLIENTE
A seguir, apresentamos um exemplo de código em Python utilizando socket para o lado servidor.
from socket import * 
s = socket(AF_INET, SOCK_STREAM) 
s.bind ((HOST, PORT)) 
s.listen(1) 
(conn, addr) = s.accept # Retorna novo socket e endereço 
client 
while True: 
 data = conn,recv(1024) # Recebe os dados do cliente 
 if not data: break # Para se o cliente para 
 conn.send(str(data)+”*” # Retorna enviando os dados + um “*” 
conn.close() # Fecha a conexão
Exemplo de um código em C para o lado servidor de um socket para ambiente Linux:
#include <stdio.h> 
#include <stdlib.h> 
#include <sys/socket.h> 
#include <arpa/inet.h> 
 
#define NRCON 5 /* Numero de conexoes */ 
#define BUFFSIZE 32 
#define PORTA 5001 
 
void TrataCliente(int sock) { 
 char buffer[BUFFSIZE]; 
 int recebido = -1; 
 /* Receive message */ 
 if ((recebido = read(sock, buffer, BUFFSIZE)) < 0) { 
 perror("erro no recebimento dos dados"); 
 exit(-1); 
 } 
 printf("Dados recebidos %s \n", buffer); 
 /* Send bytes and check for more incoming data in loop */ 
 while (recebido > 0) { 
 /* Send back received data */ 
 if (write(sock, buffer, recebido, 0) != recebido) { 
 perror("erro no envio dos dados"); 
 exit(-1); 
 } 
 /* Check for more data */
 if ((recebido = read(sock, buffer, BUFFSIZE)) < 0) { 
 perror("erro no recebimento dos dados"); 
 exit(-1); 
 } 
 printf("Dados recebidos %s \n", buffer); 
 } 
 close(sock); 
} 
 
int main(void) { 
 int ssocket, csocket; 
 struct sockaddr_in end_servidor, end_cliente; 
 
 if ((ssocket = socket(PF_INET, SOCK_STREAM, 0)) < 0) { 
 perror("Erro criação socket"); 
 exit(-1); 
 } 
 
 /* Construct the server sockaddr_in structure */ 
 memset(&end_servidor, 0, sizeof(end_servidor)); /* Clear struct */ 
 end_servidor.sin_family = AF_INET; /* Internet/IP */ 
 end_servidor.sin_addr.s_addr = htonl(INADDR_ANY); /* Incoming addr */ 
 end_servidor.sin_port = htons(PORTA); /* server port */ 
 /* Bind the server socket */ 
 if (bind(ssocket, (struct sockaddr *) &end_servidor, sizeof(end_servidor)) 
 < 0) { 
 perror("erro bind"); 
 exit(-1); 
 } 
 
 /* Listen on the server socket */ 
 if (listen(ssocket, NRCON) < 0) { 
 perror("erro listen"); 
 exit(-1); 
 } 
 /* Run until cancelled */ 
 while (1) { 
 unsigned int clientlen = sizeof(end_cliente); 
 /* Wait for client connection */ 
 if ((csocket = accept(ssocket, (struct sockaddr *) &end_cliente, 
 &clientlen)) < 0) { 
 perror("erro accept"); 
 exit(-1); 
 } 
 fprintf(stdout, "Cliente conectado: %s\n", 
 inet_ntoa(end_cliente.sin_addr)); 
 TrataCliente(csocket); 
 } 
 
 exit(0); 
} 
Exemplo de implementação de um código para o lado cliente
A seguir, apresentamos um exemplo de código em Python utilizando socket para o lado servidor:
from socket import * 
s = socket(AF_INET, SOCK_STREAM) 
s.connect((HOST, PORT)) # conecta ao servidor (bloqueia até aceitar) 
s.send('Ola, Mundo') # enviando os dados 
data = s.recv(1024) # recebe a resposta 
print data # mostra o resultado 
s.close() # fecha a sessão
Exemplo de um código em C para o lado cliente de um socket para ambientes Linux:
#include <stdio.h> 
#include <sys/socket.h> 
#include <arpa/inet.h> 
#include <stdlib.h> 
#include <string.h> 
#include <unistd.h> 
#include <netinet/in.h> 
 
#define BUFFSIZE 32 
 
int main(int argc, char *argv[]) { 
 int csocket; 
 struct sockaddr_in end_servidor; 
 char buffer[BUFFSIZE]; 
 char mensagem[15] = "Ola Mundo"; 
 unsigned int comprimento; 
 int recebido = 0; 
 
 if (argc != 3) { 
 fprintf(stderr, "Utilize: Cliente <server_ip> <port>\n"); 
 exit(1); 
 } 
 /* Create the TCP socket */ 
 if ((csocket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) { 
 perror("Erro na criacao do socket"); 
 exit(-1); 
 } 
 /* Construct the server sockaddr_in structure */ 
 memset(&end_servidor, 0, sizeof(end_servidor)); /* Clear struct */ 
 end_servidor.sin_family = AF_INET; /* Internet/IP */ 
 end_servidor.sin_addr.s_addr = inet_addr(argv[1]); /* IP address */ 
 end_servidor.sin_port = htons(atoi(argv[2])); /* server port */ 
 /* Establish connection */ 
 if (connect(csocket, (struct sockaddr *) &end_servidor, sizeof(end_servidor)) < 0) { 
 perror("erro no conect"); 
 exit(-1); 
 } 
 
 /* Send the word to the server */ 
 comprimento = strlen(mensagem); 
 if (write(csocket, mensagem, comprimento) != comprimento) { 
 perror("erro no envio de dados"); 
 exit(-1); 
 } 
 /* Receive the word back from the server */ 
 fprintf(stdout, "Recebido: "); 
 while (recebido < comprimento){ 
 int bytes = 0; 
 if ((bytes = read(csocket, buffer, BUFFSIZE - 1)) < 1) { 
 perror("erro no recebimento dos dados"); 
 exit(-1); 
 } 
 recebido += bytes; 
 buffer[bytes] = '\0'; /* Assure null terminated string */ 
 fprintf(stdout, buffer); 
 } 
 fprintf(stdout, "\n"); 
 close(csocket); 
 exit(0); 
} 
PROGRAMAÇÃO PARALELA POR INTERFACE DE
PASSAGEM DE MENSAGENS (MPI)
O conceito de programação paralela está diretamente relacionado ao uso de computadores paralelos −
dotados de várias unidades de processamento, com capacidade de executar programas paralelamente.
No caso de programas paralelos, temos instruções sendo executadas em diferentes processadores ao mesmo
tempo. Além disso, há geração e troca de informação entre os processadores.
O objetivo da programação paralela é transformar grandes algoritmos complexos em pequenas tarefas, que
possam ser executadas simultaneamente por vários processadores, reduzindo, assim, o tempo de
processamento. Mas isso demanda tempo para analisar o código a ser paralelizado, recodificar a aplicação,
depurar e esperar para se encontrar a melhor solução para o problema.
O paralelismo pode ser explorado de duas formas.
No modo implícito, existe a exploração automática do paralelismo, na qual o compilador gera as operações
paralelas. São exemplos: HPF (High Performance Fortran) e, parcialmente, alguns compiladores da Intel.
No paralelismo explícito, é dever do programador o processo de paralelizar o seu código, usando APIs como
MPI, PVM ou diretivas com o OpenMP.
As sincronizações dos processos geram muitos problemas ao programador, pois as tarefas executadas em
paralelo aguardam a finalização mútua para, só então, coordenar os resultados ou trocar dados e reiniciar
novas tarefas.
 ATENÇÃO
É necessário que haja uma perfeita coordenação do tamanho dos processos (granulosidade) e da
comunicação entre eles para que o tráfego gerado não seja maior do que o processamento, e que,
consequentemente, haja uma queda no desempenho dos processos em execução.
O MPI é uma biblioteca com funções (API) para troca de mensagens, responsável pela comunicação e
sincronização de processos em um cluster paralelo. Dessa forma, os processos de um programa paralelo
podem ser escritos em uma linguagem de programação sequencial, tal como C, Python ou Fortran.
O principal foco do MPI é disponibilizar uma interface que seja largamente utilizada no desenvolvimento de
programas que utilizem troca de mensagens. Além de garantir a portabilidade dos programas paralelos, essa
interface deve ser implementada eficientemente nos diversos tipos de máquinas paralelas existentes, sejam
elas supercomputadores dedicados ou por meio de clusters de computadores do mercado.
O objetivo principal é que o MPI se torne o padrão de interface mais utilizado em sistemas distribuídos, no
qual cada processador executa um processo paralelo seguindo o modelo SPMD (Single Program Multiple
Data – Único Programa sobre Múltiplos Dados). Isso porque o MPI define um conjunto de rotinas para facilitar
a comunicação (troca de dados e sincronização) entre processos em memória. Ele é portável para qualquer
arquitetura, tem aproximadamente 300 funções para programação e ferramentas para análise de
desempenho.
 COMENTÁRIO
A biblioteca MPI possui rotinas para programas em linguagem C / C++, Python, Fortran 77/90 e muitas outras
linguagens de programação. Os programas são compilados e ligados à biblioteca MPI. Inicialmente, na
maioria das implementações, um conjunto fixo de processos é criado.
Os elementos mais importantes em implementações paralelas são:
A comunicação de dados entre processos paralelos.
O balanceamento da carga.
Os processos podem usar mecanismos de comunicação ponto a ponto em operações para enviar mensagens
de um determinado processo a outro. Um grupo de processos pode chamar operações coletivas de
comunicação para executar operações globais.
O MPI é capaz de suportar comunicação assíncrona e programação modular por meio de mecanismos de
comunicadores que permitem ao usuário MPI definir módulos que englobem estruturas de comunicação
interna. Cada processo executa e comunica-se com outras instâncias do programa, possibilitando a execução
no mesmo processador ou em diferentes processadores. Essas instâncias podem se dar por meio de
comunicação ponto a ponto ou coletiva.
O melhor desempenho acontece quando esses processos são distribuídos entre diversos processadores. A
comunicação básica consiste em enviar e receber dados de um processador para outro. Essa comunicação se
dá por meio de uma rede de alta velocidade (como InfiniBand ou Ethernet 10G), na qual os processos estarão
em um sistema de memória distribuída. O pacote de dados enviados pelo MPI requer vários pedaços de
informações:
O processo transmissor.
O processo receptor.
O endereço inicial de memória para onde os itens de dados deverão ser mandados.
A mensagem de identificação.
O grupo de processos que podem receber a mensagem.
Ou seja, tudo isso ficará sob responsabilidade do programador em identificar o paralelismo e implementar um
algoritmo utilizando construções com o MPI.
O programa “Olá Mundo” (Hello World), a seguir, apresenta a estrutura básica de um programa MPI escrito em
C:
#include “mpi.h” 
#include <stdio.h> 
int main(int argc,char *argv[]) { 
int meu_id, numero_processos; 
 
// Rotinas de inicialização 
MPI_Init(&argc,&argv); 
MPI_Comm_size(MPI_COMM_WORLD, &numero_processos); 
MPI_Comm_rank(MPI_COMM_WORLD, &meu_id); 
 
fprintf(stdout,”Olá mundo! Sou o processo %i de %i criados”, meu_id, numero_processos); 
// Rotinas de finalização 
MPI_Finalize(); 
return 0; 
} 
A primeira linha desse programa inclui a biblioteca do MPI em C. O comando MPI_Init, com os argumentos
passados pela linha de comando do mpirun, inicializa o ambiente MPI para esse processo.
O comando MPI_Comm_size(MPI_COMM_WORLD, &numero_processos) retorna na variável
"numero_processos" o número total de processos criados.
O comando MPI_Comm_rank(MPI_COMM_WORLD, &meu_id) retorna na variável "meu_id" a identificação
única desse processo, dentre os n processos criados.
 ATENÇÃO
É importante destacar que os valores retornados variam de 0 até o número total de processos –1.
O parâmetro MPI_COMM_WORLD, usado por esses comandos, identifica um objeto local, que representa o
contexto de uma comunicação. Processos podem se comunicar, desde que estejam no mesmo grupo de
comunicação. 
O comando MPI_Finalize() encerra o ambiente MPI para esse processo.
Nesse exemplo, cada um desses n processos criados executará o comando fprintf. Consequentemente, n
mensagens serão impressas. A identificação do processo usado nesse comando serve para diferenciar qual
processo escreveu qual mensagem.
Exemplo de código MPI bloqueante
Nesse exemplo, é explorada a comunicação síncrona, na qual o processador que envia a mensagem aguarda
um sinal do destinatário confirmando o recebimento da mesma.
# include <mpi .h> 
# include <stdio .h> 
# define ROOT 0 
# define MSGLEN 100 
int main (int argc , char ** argv ) 
{ 
 int i, rank , size , nlen , tag = 999; 
 char name [ MPI_MAX_PROCESSOR_NAME ]; 
 char recvmsg [ MSGLEN ], sendmsg [ MSGLEN ]; 
 MPI_Status stats ; 
 MPI_Init (& argc , & argv ); 
 MPI_Comm_rank ( MPI_COMM_WORLD , & rank ); 
 MPI_Comm_size ( MPI_COMM_WORLD , & size ); 
 MPI_Get_processor_name (name , & nlen ); 
 sprintf ( sendmsg , " Hello world ! I am process %d out of %d on %s\n", rank , size , name ); 
 if ( rank == ROOT ) { 
 printf (" Message from process %d: %s", ROOT , sendmsg ); 
 for (i = 1; i < size ; ++i) { 
 MPI_Recv (& recvmsg , MSGLEN , MPI_CHAR , i, tag , MPI_COMM_WORLD , & stats ); 
 printf (" Message from process %d: %s", i, recvmsg ); 
 } 
 } else { 
 MPI_Send (& sendmsg , MSGLEN , MPI_CHAR , ROOT , tag , MPI_COMM_WORLD ); 
} 
MPI_Finalize (); 
return 0; 
} 
Exemplo de código MPI não bloqueante
Nesse exemplo, é exploradaa comunicação assíncrona, na qual o processo que envia a mensagem não
espera que haja um sinal de recebimento desta pelo destinatário.
# include <stdio .h> 
# define ROOT 0 
int main (int argc , char ** argv ) 
{ 
 int rank , size , next , prev , buf [2] , tag1 =999 , tag2 =777; 
 MPI_Request reqs [4]; 
 MPI_Status stats [4]; 
 
 MPI_Init (& argc , & argv ); 
 MPI_Comm_rank ( MPI_COMM_WORLD , & rank ); 
 MPI_Comm_size ( MPI_COMM_WORLD , & size ); 
 
 prev = rank -1; 
 next = rank +1; 
 if ( rank == ROOT ) prev = size - 1; 
 if ( rank == ( size - 1)) next = ROOT ; 
 
 MPI_Irecv (& buf [0] , 1, MPI_INT , prev , tag1 , MPI_COMM_WORLD , & reqs [0]) ; 
 MPI_Irecv (& buf [1] , 1, MPI_INT , next , tag2 , MPI_COMM_WORLD , & reqs [1]) ; 
 
 MPI_Isend (& rank , 1, MPI_INT , prev , tag2 , MPI_COMM_WORLD , & reqs [2]) ; 
 MPI_Isend (& rank , 1, MPI_INT , next , tag1 , MPI_COMM_WORLD , & reqs [3]) ; 
 
 /* do some work */ 
 MPI_Waitall (4, reqs , stats ); 
 
 MPI_Finalize (); 
 return 0; 
}
CHAMADA A PROCEDIMENTO REMOTO (RPC)
Como vimos até o momento, sistemas distribuídos, em geral, são baseados na troca de mensagens entre
processos. Dentre os mecanismos de troca disponíveis, as chamadas de procedimento remoto ou RPC
(Remote Procedure Calls) são consideradas até o momento como um pilar básico para a implementação de
boa parte dos requisitos de sistemas distribuídos baseados em cliente/servidor.
O princípio do RPC é estender a noção da chamada de procedimento local para a chamada remota.
Em uma chamada de procedimento local, um processo faz uma requisição para um outro e envia argumentos
definidos.
O processo que recebeu a chamada irá executar alguma operação e devolver o resultado para o processo que
o chamou.
A chamada de procedimento remota é similar, mas os processos podem estar no mesmo sistema ou em
diferentes sistemas conectados em uma rede.
Uma chamada de procedimento remoto é iniciada pelo cliente, que envia uma mensagem para um servidor
remoto para executar um procedimento específico.
Então, uma resposta é retornada ao cliente.
Uma diferença importante entre chamadas de procedimento remotas e locais é que, no primeiro caso, a
chamada pode falhar por problemas da rede.
Nesse caso, não há nem mesmo garantia de que o procedimento foi invocado.
 Estrutura de RPC cliente/servidor.
No modelo RPC, um thread é responsável pelo controle de dois processos: cliente e servidor.
O processo cliente envia uma mensagem ao processo servidor e aguarda (bloqueando) uma resposta. A
mensagem do cliente contém os parâmetros do procedimento, e a de resposta contém o resultado da
execução do procedimento. Uma vez que a mensagem de resposta é recebida, os resultados da execução do
procedimento são coletados e a execução do cliente prossegue.
 Comunicação RPC cliente/servidor.
Do lado do servidor, um processo permanece em espera até a chegada de uma mensagem do cliente.
Quando esta é recebida, o servidor extrai os parâmetros, processa-os e produz os resultados, que são
enviados na mensagem de resposta. O servidor, então, volta a esperar por uma nova mensagem de outros
clientes. Nesse modelo, apenas um dos dois processos permanece ativo, em um dado instante de tempo.
O protocolo RPC pode ser implementado sobre diferentes tipos de protocolos de transporte, uma vez que é
indiferente à maneira como uma mensagem é transmitida entre os processos.
 ATENÇÃO
O protocolo RPC não implementa nenhuma forma de confiabilidade, é a aplicação que precisa tomar cuidados
quanto ao tipo de protocolo sobre o qual ele opera. Para o RPC, não é importante como a mensagem é
enviada de um processo ao outro, mas sim a especificação e a interpretação das mensagens.
Ao requisitar um procedimento remoto, deve-se observar que o cliente e o servidor podem ser de plataformas
diferentes, que representam dados de formas diferentes. É necessário, portanto, um protocolo comum de
representação dos dados, como o XDR, ou a garantia de que ambas as partes saibam converter os dados.
Por ser uma chamada remota, em outro espaço de endereçamento, deve-se atentar também para o desafio
de passar um ponteiro. Nesse caso, a implementação interna do RPC deve passar o conteúdo do ponteiro por
cópia e restaurar a área de memória no retorno do procedimento.
 RESUMINDO
Uma chamada de procedimento remota é feita por um processo a uma função ou procedimento remoto, envia
os argumentos na mensagem e aguarda uma resposta. A função remota recebe os argumentos, processa
instruções e devolve o resultado ao processo solicitante.
Cada procedimento remoto é identificado de forma única pelo número do programa, número de versão e
número de procedimento. Para que um programa cliente possa fazer uma chamada de procedimento remoto,
ele precisa achar a porta do programa servidor para se conectar. Os protocolos de serviço não fazem isso,
uma vez que sua obrigação se limita a transportar as mensagens de um lugar para o outro. Será necessário,
portanto, um socket para tal.
Cada sistema possui o seu próprio método de escolha de qual número de porta o mesmo irá usufruir para
cada processo. Então, é necessário estabelecer um protocolo para mapeamento das portas e os serviços
oferecidos, independentemente das convenções do sistema. Esse protocolo é denominado mapeador de
portas – portmapper. A aplicação que implementa esse mapeador será responsável por mapear as chamadas
de RPC, as versões dessas chamadas e as portas específicas selecionadas. Normalmente, sistemas
Unix/Linux utilizam uma aplicação denominada portmap ou rpcbind na porta 111 sobre TCP ou UDP.
São exemplos de serviços que usam RPC:
NFS para compartilhamento de arquivos (Network File Server).
Sistema de autenticação NIS (Network Information Service).
DESENVOLVIMENTO COM MEMÓRIA
COMPARTILHADA (BARRAMENTO)
Os sistemas de processamento simétrico (SMP) possuem de dois a sessenta e quatro processadores, e são
considerados uma arquitetura de memória compartilhada (multiprocessador com memória única global). Cada
processador tem acesso a toda a memória do sistema por meio de um barramento ou de uma rede de
comunicação dedicada.
O modelo ideal para uma memória compartilhada por vários processadores seria um tempo de acesso baixo,
em um só ciclo de relógio e com largura de banda infinita.
Na realidade, os tempos de acesso não são desprezíveis, crescem com o tamanho do sistema, e a largura de
banda é limitada. Uma das soluções aplicadas para contornar essa última situação passa por utilizar lógica
adicional para interligações, o que aumenta os tempos de acesso. Essa arquitetura é limitada ao nível de
processadores, devido ao problema clássico de saturação do barramento e coerência na memória cache.
Qualquer processo ou processador pode ler ou escrever qualquer palavra na memória compartilhada,
simplesmente movendo os dados para o local determinado. A sincronização entre tarefas é feita por
escrita/leitura na memória compartilhada por meio de instruções de load/restore (carregar/recuperar), a
comunicação entre tarefas é rápida e a escalabilidade é limitada pelo número de caminhos entre a memória e
os processadores, podendo saturar o barramento de comunicação.
O desenvolvimento de aplicações paralelas nesse modelo pode ser feito tendo como base a biblioteca
OpenMP. A API disponibilizada estende as linguagens de programação sequenciais (C/C++/Fortran),
acrescentando-lhes estruturas SPMD (Simple Program Multiple Data – Programa Único Dados Múltiplos), isto
é, estruturas que simplificam o compartilhamento de dados entre processos cooperativos. Possuem, ainda,
primitivas de sincronização, de compartilhamento e de privatização dos dados, gerando todo o ambiente
necessário para facilitar a vida dos programadores.
PROGRAMANDO EM OPENMP
OpenMP é uma interface de programação (API) multithreading, portável, baseada no modelo de programação
paralela de memória compartilhada para arquiteturas de múltiplos processadores, sendo suportadapor uma
variedade de plataformas.
Sua função essencial é estabelecer um conjunto limitado e simples de diretivas para programação utilizando
memória compartilhada por barramento.
É composta por três componentes básicos:
Diretivas de compilação;
Bibliotecas de execução;
Variáveis de ambiente.
Essa interface permite o paralelismo explícito, que possibilita ao programador total controle sobre a
paralelização do código. Possui suporte e apoio de grandes empresas mundiais e universidades na
preparação de sua estrutura. Atualmente, suporta Fortran, C e C++.
Todos os programas OpenMP iniciam como processo simples, denominado “thread mestre”, que executa
sequencialmente até a primeira definição (diretiva) de uma região paralela ser acionada. O thread mestre cria
um fork (bifurcação), gerando um conjunto de threads paralelos. Assim, os comandos de um programa
injetados na região paralela serão executados pelos diversos threads criados e, ao seu término, os mesmos
são sincronizados e finalizados, permanecendo somente o thread mestre principal. Os threads se comunicam
por meio de variáveis compartilhadas.
 ATENÇÃO
Todo o paralelismo do OpenMP é baseado em diretivas de compilação. Dependendo de sua implementação, o
OpenMP permite que, em uma região paralela, existam outras regiões que também podem ser executadas em
paralelo.
ESTRUTURA DE UM CÓDIGO EM OPENMP
A seguir, apresentamos a estrutura de um programa utilizando o OpenMP. No programa, deve ser definido o
trecho que será paralelizado utilizando a diretiva #pragma omp:
#include <omp.h> 
main () { 
 int var1, var2, var3; 
 *** Código serial 
 . 
 . 
 *** Início da seção paralela. “Fork” um grupo de “threads”. 
 
 #pragma omp parallel private(var1, var2) shared(var3) 
 
 { 
 *** Seção paralela executada por todas as “threads” 
 . 
 . 
 *** Todas as “threads” efetuam um “join” a thread mestre e finalizam 
 } 
 
 *** Código serial 
 . 
 . 
}
Tomamos um exemplo de um código sequencial:
for (i=0; i < N; i++) 
 a[i] =a [i] + b[i]; 
Transformando em um código paralelizado com OpenMP:
#pragma omp parallel 
#pragma omp for 
 for (i=0; i < N; i++) a[i] = a[i] + b[i];
Existem infinitas diretivas e técnicas de desenvolvimento para sincronização com o OpenMP. É uma
metodologia bem interessante, que pode se acoplar, por exemplo, ao MPI, para desenvolvimento híbrido de
aplicações paralelas por passagens de mensagens sobre processadores multicores.
INTERFACES DE PROGRAMAÇÃO DISTRIBUÍDA
Apresentamos, no vídeo a seguir, exemplos de programas que utilizam as APIs de programação distribuída
Socket, RPC, MPI e OpenMP.
VERIFICANDO O APRENDIZADO
CONCLUSÃO
CONSIDERAÇÕES FINAIS
Embora o paradigma da computação distribuída seja muito utilizado no meio acadêmico para pesquisas em
novas tecnologias, é no meio empresarial que revela sua maior ênfase. Neste, é utilizado em plataformas de
serviços e aprovisionamento, produção e distribuição de conteúdo multimídia, alojamento de servidores
aplicacionais, supercomputadores, suporte a tecnologias de segurança e em plataformas de processamento
de grandes fluxos de dados como Big Data, dentre muitas outras.
Identificar a concorrência entre componentes para detectar possibilidades de paralelismo, proporcionar
compartilhamento seguro de recursos e entregar um sistema de imagem única, tratando o ambiente de forma
transparente aos seus usuários independentemente de localização, com replicação automática entre
aplicativos, segurança, desempenho e alinhamento com as estratégias do negócio são alguns dos vários
desafios acoplados ao crescimento contínuo do mundo da computação distribuída. Nele, podemos verificar o
aumento exponencial de soluções de inteligência artificial, computação quântica, Blockchain, dentre muitas
outras que fazem uso massivo da computação distribuída.
Por isso, um aprofundamento em alguns desses temas é fundamental para o seu crescimento, tanto na visão
acadêmica, quanto na visão corporativa.
 PODCAST
Para finalizar, ouça o podcast a seguir. Nele, apresentamos um resumo de todo o conteúdo apresentado.
AVALIAÇÃO DO TEMA:
REFERÊNCIAS
BRZEZINSKI, J. et al. Deadlock models and a general algorithm for distributed deadlock detection.
Journal of parallel and distributed computing, 31(2):112–125. 1995.
CHANDRA, R. Parallel programing in OpenMP. Massachusetts: Morgan Kaufmann, 2001.
CONDOR. An introduction to the Message Passing Interface (MPI) using C. Consultado na Internet em: 17
jun. 2021.
COULOURIS, G.; DOLLIMORE, J.; KINDBERG, T. Sistemas distribuídos. 4 ed. São Paulo: Artmed, 2007.
CRISTIAN, F. Probabilistic clock synchronization. Distributed Computing, Springer-Verlag, 1989. 3 (3): 146–
158, doi:10.1007/BF01784024.
DE ROSE, C. A. F. Arquiteturas paralelas. Pontifícia Universidade Católica do Rio Grande do Sul, 2001.
GOULART, A. Sistemas distribuídos e comunicação em grupo. Universidade do Vale do Itajaí, 2002.
JUNIOR, A. C. G. Autoajuste de parâmetros em protocolo de comunicação em grupo: estudo de caso do
Isis. Consultado na Internet em: 01 jun. 2021.
KSHEMKALYANI, A. D.; SINGHAL, M. Efficient detection and resolution of generalized distributed
deadlocks. IEEE Transactions on Software Engineering, 20(1):43–54. 1994.
KUMAR, M.; SINGH, C. Building data streaming applications with Apache Kafka. Birmingham: Packt
Publishing, 2017. 
LAMPORT, L. Time, clocks, and the ordering of events in a distributed system. Communications of the
ACM, 1978. 21(7).
NARKHEDE, N.; SHAPIRA, G.; PALINO, T. Kafka: the definitive guide. Massachusetts: O’Reilly Media, 2016.
PITANGA, M. Construindo supercomputadores com Linux. 3. ed. Rio de Janeiro: Brasport, 2008.
TANENBAUM, A. S.; STEEN, M. V. Sistemas distribuídos. 3. ed. Rio de Janeiro: Prentice Hall, 2017.
EXPLORE+
Para saber mais sobre os assuntos tratados neste conteúdo, pesquise:
A lista dos 500 supercomputadores mais rápidos do mundo.
O MPI Forum.
A ferramenta Zookeeper.
As apresentações disponíveis na Internet, para aprender um pouco mais sobre OpenMP.
CONTEUDISTA
Sergio Rodrigues Affonso Franco
 CURRÍCULO LATTES
javascript:void(0);

Mais conteúdos dessa disciplina