técnicas de simulação distribuída

53
Técnicas de Simulação Distribuída Trabalho apresentado na Pós Graduação em Ciência da Computação Universidade Federal Fluminense Instituto de Computação Disciplina: Laboratório de Programação Paralela Professora: Lúcia Drummond Grupo: Aline de Paula Nascimento Juliana Nascente Mário Teles Rodrigo Santos Niterói, 17 de dezembro de 2003.

Upload: others

Post on 18-Apr-2022

3 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída

Trabalho apresentado na Pós Graduação em Ciência da ComputaçãoUniversidade Federal Fluminense

Instituto de Computação

Disciplina: Laboratório de Programação Paralela

Professora: Lúcia Drummond

Grupo: Aline de Paula Nascimento Juliana Nascente Mário Teles Rodrigo Santos

Niterói, 17 de dezembro de 2003.

Page 2: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída - 2

Índice

1. Introdução______________________________________________ 3

2. Simulação Time-Stepped ___________________________________ 6

3.1 Totalmente Concorrente __________________________________ 6

3.2 Parcialmente Concorrente_________________________________ 8

3.3 O Problema de N-corpos ________________________________ 10

3.3.1 O Algoritmo de Barnes-Hut _____________________________11

3.3.2 Simulação Seqüencial_________________________________ 12

3.3.3 Simulação Paralela___________________________________ 13

3.4 A Implementação do Problema ____________________________ 15

3.4.1 Considerações gerais_________________________________ 15

3.4.2 O Algoritmo _______________________________________ 20

3. Simulação Event-Driven___________________________________ 21

3.1 Conservativa __________________________________________ 21

3.2 Otimista _____________________________________________ 27

3.3 O Problema de Filas ____________________________________ 31

3.4 A Implementação do Problema ____________________________ 33

3.4.1 Considerações Gerais ________________________________ 33

3.4.2 O Algoritmo _______________________________________ 37

4. Considerações Finais _____________________________________ 38

5. Referências Bibliográficas _________________________________ 38

Anexo A___________________________________________________ 39

Page 3: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída - 3

SIMULAÇÃO DISTRIBUÍDA

Um sistema físico é uma coleção de entidades físicas chamadas processos físicos. A

idéia principal deste trabalho é o estudo de algoritmos distribuídos que determinem,

através de simulação, os estados dos processos físicos em cada instante em um

determinado intervalo de tempo. Os estados destes processos físicos mudam, pois eles

interagem uns com os outros. Serão apresentadas duas formas de simulação distribuída, a

primeira baseada em intervalos de tempo (time-stepped) e a outra orientada a eventos

(event driven).

O trabalho está dividido da seguinte maneira. Na seção 1 além de ser detalhado o

modelo do sistema físico e lógico, são mostrados outros conceitos relacionados ao

problema de simulação. A seção 2 é dedicada à simulação baseada em intervalos de tempo,

nela é explicado o problema de N-corpos, além de se mostrar como foi feita a sua

implementação. Na seção 3 é estudado o problema da simulação orientada a eventos que é

exemplificado com o problema de eventos em filas em um sistema operacional. Na seção 4

e 5 seguem as considerações finais e referências do trabalho. Os algoritmos

implementados em C com MPI podem ser vistos no apêndice.

1. Introdução

O sistema físico pode ser representado tanto por um grafo não direcionado

G = (N, E) como por uma grafo direcionado G = (N, D), onde N é o conjunto dos

processos físicos. O grafo G é considerado não direcionado, se e somente se para todas as

tarefas vi, vj ∈ N tal que vi ≠ vj, vi afeta o estado de vj e vj afeta o estado de vi. Neste

caso, (vi, vj) é uma aresta em E. No caso em que apenas vi afeta o estado de vj (o inverso

não se aplica) o grafo em questão é direcionado e (vi →vj) ∈ D.

O estado de um processo físico vi, em um tempo t ≥ 0 é denotado por xi(t). O

objetivo da simulação de um sistema físico é determinar xi(t) para todo vi ∈ N, onde

0 ≤ t ≤ T. O valor de T deve ser determinado previamente ou enquanto a simulação

Page 4: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída - 4

progride. Neste trabalho t é considerado um inteiro não negativo que varia até T.

A simulação de um sistema físico G é obtida através de um sistema lógico, que

contém um processo lógico para cada processo físico de G. Os processos lógicos tentam

imitar a interação que ocorre entre os processos físicos em um sistema físico. A

abordagem usada de como um processo lógico simula um processo físico, depende de como

os processos físicos interagem entre si. Esta interação pode ser dividida em duas classes:

time-stepped (intervalos de tempo) e event-driven (orientada a eventos).

Se os processos físicos interagem continuamente de forma que os instantes nos

quais o estado de um processo físico pode mudar, possa ser determinado previamente

então o ponto principal do sistema lógico é o tempo. Neste caso, a simulação empregada

pelo sistema lógico é chamada time-stepped. Porém, existem casos onde mudanças no

estado de um processo físico são restritas aos instantes nos quais os processos

interagem, e tais instantes só podem ser conhecidos a medida que a simulação progride.

Nestes casos a simulação é event-driven.

Sendo n = |N|, e para 1 ≤ i ≤ n seja o nó ni o processo lógico que simula o

comportamento do processo físico vi. G é o grafo cujo conjunto de arestas deve conter as

arestas de G, porém G pode ser considerado um grafo completo não direcionado se a

estrutura de G na for conhecida.

A essência de uma simulação time-stepped é simples. Para cada instante no qual o

estado de um processo físico vi, pode mudar, o processo lógico correspondente faz uma

atualização baseada no estado corrente de vi e no estado de todos outros processos

físicos que exercem influência no estado de vi (devem ser consideradas as arestas do

grafo e se ele é direcionado ou não).

Para facilitar a explicação da simulação event-driven, pode se dar inicialmente um

exemplo considerando uma versão seqüencial. Em uma simulação event-driven seqüencial, é

empregada uma fila de eventos. Nesta fila os eventos são colocados em ordem não

decrescente do tempo em que eles podem ocorrer. A tarefa do simulador seqüencial é

interagir com esta fila até que ela esteja vazia ou até que não existam mais eventos

escalonados para acontecerem antes do tempo T. Em cada iteração, o simulador remove o

primeiro evento da fila, atualiza o estado do processo físico correspondente, e pode ainda

adicionar na fila novos eventos que podem acontecer futuramente em algum outro

Page 5: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída - 5

processo físico. Inicialmente, a fila contém apenas os eventos que podem acontecer

espontaneamente, que são assumidos acontecerem no tempo zero.

Na simulação distribuída, uma das possibilidades é que cada processo lógico

mantenha uma fila similar com os eventos que podem estar escalonados para acontecerem

no processo físico ao qual ele corresponde. Inicialmente apenas um processo lógico cujos

eventos dos processos físicos acontecem espontaneamente não possui uma fila vazia. O

conjunto de tais processos físicos é N0 ⊆ N e a fila é mantida em ordem crescente de

tempo. Assume-se que dois eventos não podem acontecer em um mesmo tempo em um

mesmo processo físico, por isso, não existe a necessidade de uma fila não decrescente,

como visto na simulação seqüencial.

A tarefa do processo lógico é similar ao simulador seqüencial, remover o primeiro

evento da fila, atualizar o estado do processo físico e em seguida enviar novos eventos

para as filas de outros processos lógicos. O problema é assegurar que os eventos serão

processados globalmente em uma ordem não decrescente de tempo, ou caso isto não possa

ser assegurado, que existam meios para corrigir erros como o de um processo lógico

receber um evento escalonado para um determinado tempo que já tenha passado na

simulação local.

Na simulação time-stepped de G, as mensagens que os nós trocam são simplesmente

o estado inicial ou atualizado dos processos físicos correspondentes. Na simulação event-

driven, uma mensagem enviada de um nó ni para um nó nj é um evento que o processo físico

vi causa e que deve acontecer no processo físico vj. Tal mensagem é denotada por event(t),

onde t é o tempo no qual o evento irá acontecer em vj.

Algumas propriedades devem ser estabelecidas em relação ao conjunto de eventos

em uma computação distribuída. Uma propriedade estabelece que se um evento for gerado

em um tempo t por um processo físico vi para acontecer em um tempo t’ em um processo

físico vj, então t < t’. Outra propriedade fundamental é se t e t’ são tempos nos quais dois

eventos acontecem em um certo processo físico então t < t’ ou t’ < t.

A tarefa de um simulador event-driven é assegurar para cada processo físico, que

nenhum evento poderá ser processado antes que todos os eventos que o precedam,

naquele processo físico, já tenham sido processados.

Page 6: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída - 6

2. Simulação Time-Stepped

Nesta seção serão apresentadas duas linhas principais da simulação baseada em

intervalos de tempo (time-stepped). As duas variações, motivadas pelas características

dos sistemas físicos, são: totalmente concorrente e parcialmente concorrente.

Na simulação time-stepped há um sincronismo entre as ações, com isto a cada

instante os processos executam um conjunto de ações para depois trocarem seus estados

para que no próximo passo estes dados sejam utilizados para a execução das ações, esta

seqüência se repete até uma condição de parada ser satisfeita.

Para os algoritmos apresentados, considera-se que a estrutura de G é conhecida, ou

seja, todos os vértices e arestas do grafo do sistema físico devem ser conhecidos e esta

estrutura não pode ser modificada dinamicamente, durante a simulação. Os grafos

representativos do sistema físico e do sistema lógico são isomorfos (seus conjuntos de

vértices e arestas devem ser idênticos).

3.1 Totalmente Concorrente

A cada iteração o novo estado do processo é calculado através de uma função dos

estados antigos dos seus vizinhos e do seu estado antigo. Neste tipo de simulação a

atualização do estado de um processo torna-se totalmente independente da atualização

dos outros estados, ou seja, ao calcular-se o estado no passo i os dados necessários são,

somente, os dados do passo i-1.

Alguns exemplos de simulação time-stepped com concorrência total são: Alguns tipos

de redes neurais, sistemas de equações diferenciais, alguns métodos numéricos iterativos,

o problema de N-corpos, entre outros.

De uma forma geral, pode se representar este tipo de simulação pelos seguintes

passos de execução:

1. Se houver necessidade um processo é designado para fazer uma inicialização

do sistema distribuído.

2. Cada processo faz inicialização de suas variáveis e calcula o seu estado inicial.

3. Os processos entram em um loop no qual a condição de parada é a satisfação

com o resultado obtido.

Page 7: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída - 7

3.1 Enviar mensagens contendo o estado atual para todos os vizinhos

3.2 Receber os estados de todos os vizinhos

3.3 Atualizar os estados utilizando os valores recebidos

A seguir é apresentado o algoritmo:

Algorithm A_Simulate_FC; Variables:

Ti = 0;

statei = xi(0);

stateji = nil for all nj ∈ Neigj;

next_stateji = nil for all nj ∈ Neigj;

initiatedi = false.

Input: msgi = nil;

Action if ni ∈ N0:initiatedi = true;

Send statei to all nj ∈ Neigi.

Input:msgi = x such that origini (msgi) = (ni, nj).

Action:If not initiatedi then

begin

initiatedi := true;

Send statei to all nk ∈ Neigi

end;

if stateji ≠ then

next_stateji := x

else

stateji := x;

if stateki ≠ nil for all nk ∈ Neigi then

begin

Ti = Ti + 1;

Update statei;

Send statei to all nk ∈ Neigi;

for all nk ∈ Neigi do

begin

stateki := next_stateki;

next_stateki := nil

end

end.

Page 8: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída - 8

3.2 Parcialmente Concorrente

Em um sistema físico parcialmente concorrente, xi(0) é gerado por todo processo

físico vi, porém devido a restrição de vizinhança (neighborhood constraint) o valor de t é

restringido pelo estado que o processo físico vai ser atualizado. Especificamente, dois

processos físicos só podem ter seus estados atualizados em um mesmo t se, e somente se,

não houver dependência entre eles.

Nesta seção, analisaremos um algoritmo que utiliza esta técnica usando o exemplo do

problema dos Filósofos Comilões (Dinning Philosophers). Neste algoritmo, que explora o

compartilhamento de recursos, deseja-se resolver o seguinte problema:

• Em torno de uma mesa existem N filósofos (processos) e N garfos (recursos).

• Os filósofos ou estão pensando ou estão com fome ou estão comendo.

• Os garfos são compartilhados entre os filósofos para que estes possam comer.

Nenhum filósofo come indefinidamente.

• Um filósofo só poderá comer se estiver segurando os dois garfos, adjacentes a ele.

• Cada filósofo compartilha um garfo com o filósofo ao seu lado.

• Não existe uma hierarquia entre os filósofos que permita resolução de conflitos.

• Após comer o filósofo libera os garfos para seus vizinhos.

Como é possível observar no problema acima, dois filósofos (nós) vizinhos não podem

Filósofo2Filósofo5

Filósofo1

Filósofo3Filósofo4

Figura 1: Dinning Philosophers

Page 9: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída - 9

comer (se atualizarem) ao mesmo tempo, respeitando assim as restrições de vizinhança.

Um algoritmo genérico para representar este tipo de simulação pode ser dado da

seguinte forma:

1. Inicializar variáveis

2. Caso o nó esteja no conjunto dos nós que inicia sua computação

espontaneamente, enviar seu estado para seus vizinhos.

3. Caso um nó receba o estado de um vizinho:

3.1 Verificar se é a primeira mensagem recebida (estado recebido está vazio).

Se for, mandar seu estado para os vizinhos.

3.2 Caso seja um estado válido (estado recebido diferente de vazio), marcar

que já possui o estado de determinado vizinho.

3.3 Se já possui os estados de todos os seus vizinhos, passa para o próximo

passo (atualizar a unidade de tempo), atualizar seu estado, desmarcar que

recebeu os estados dos vizinhos e mandar seu novo estado para todos os

vizinhos.

Para a elaboração desse algoritmo, três variáveis têm papel importante:

• T – Contador de tempo (iterações)

• State – Estado do processo

• Upstream – Indica a orientação corrente dos canais e armazena se um

processo já recebeu o estado de um determinado nó (só após receber o

estado de todos os nós vizinhos um processo pode ser atualizado e passar

pra próxima iteração).

Dessa forma, temos o seguinte algoritmo:

Algorithm A_Simulate_PC; Variables:

Ti = 0;

statei = xi(0);

stateji = nil for all nj ∈ Neigj;

upstreamji for all nj ∈ Neigi.

Page 10: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída - 10

3.3 O Problema de N-corpos

Técnicas clássicas de N-corpos estudam a evolução de um sistema de N-corpos,

onde existe uma força entre os pares de partículas (por exemplo, a força gravitacional).

Métodos hierárquicos reduzem a complexidade do problema de O(n2) para O(nlogn)

baseando no seguinte princípio de Newton: "Se a magnitude de interação entre os corpos

cai rapidamente com a distância, então o efeito de um aglomerado de corpos sobre uma

partícula deve ser aproximado ao efeito de um simples corpo, se o grupo de corpos estiver

longe o bastante dessa partícula".

A simulação deste problema está baseada no algoritmo de Barnes-Hut e segue

sobre o número de iterações, onde a cada iteração, acontece o cálculo das forças em cada

corpo e a atualização de sua posição e outros atributos.

Input: msgi = nil;

Action if ni ∈ N0: Send statei to all nj ∈ Neigi.

Input:msgi = x such that origini (msgi) = (ni, nj).

Action:if stateki = nil for all nk ∈ Neigi then

Send statei to all nk ∈ Neigi;

if stateji ≠ nil then

upstreamji := true;

stateji := x;

if (stateki ≠ nil and upstreamki) for all nk ∈ Neigi then

begin

Update Ti;

Update statei;

upstreamki := false for all nk ∈ Neigi;

Send statei to all nk ∈ Neigi.

end.

Page 11: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída - 11

3.3.1 O Algoritmo de Barnes-Hut

Uma partícula é composta dos seguintes atributos: massa, posição e velocidade e

está sujeita a uma força em virtude das outras partículas que também compõem o espaço.

O algoritmo de Barnes-Hut tem como objetivo simular o movimento das partículas

no tempo. Um método é calcular o acúmulo dos efeitos das forças entre os pares de

partículas: para cada par a força este acúmulo é calculado usando as leis de Newton.

A ordem de complexidade do problema pode ser reduzida de O(n2) para O(nlogn),

utilizando o seguinte princípio: um grupo de corpos que está longe de uma partícula i pode

ser modelado usando uma única partícula j cuja massa é a massa total do grupo de corpos

e cuja posição é o centro e de massa do grupo. Para tanto, a cada passo da simulação uma

árvore é construída usando partículas do passo anterior e o espaço é, recursivamente,

dividido até que não mais de uma partícula esteja presente em cada divisão. Os nós

intermediários são usados para acumular a massa total e o centro de massa de cada grupo.

As figuras a seguir mostram a divisão do espaço físico e a construção da árvore

correspondente.

A aceleração de uma partícula é calculada descendo a árvore e aplicando o critério

de precisão em cada nó. Se um nó provê uma aproximação suficientemente exata dos

efeitos em uma partícula, então a aceleração é calculada pela interação do nó e da

partícula, senão é calculada a somatória das contribuições dos filhos do nó.

Figura 2: Divisão recursiva de uma área até que cada parte contenhaapenas uma partícula.

Page 12: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída - 12

3.3.2 Simulação Seqüencial

Toda a informação a respeito das partículas é representada em forma de árvore

binária e o procedimento Orthogonal Recursive Bisection (ORB) - responsável pela

formação de uma árvore binária de corpos, pode ser usado.

O procedimento inicia na raiz que representa o domínio de todas as partículas

pertencentes a ele. Os níveis inferiores da árvore são formados recursivamente pela

divisão do domínio em dois subdomínios retangulares contendo o mesmo número de

partículas até que os subdomínios finais contenham somente uma partícula. Logo, as folhas

representam as partículas e os nós intermediários representam um grupo de partículas.

O próximo passo da simulação consiste em associar a cada nó o centro de massa e a

massa equivalente das partículas. Isso é feito pela propagação da informação das folhas

para os nós pais até chegar na raiz.

No passo seguinte, uma vez que as folhas já contêm informações das partículas e os

nós contêm informações sobre o grupo de partículas, a árvore pode ser usada, da raiz para

as folhas, para o cálculo da força resultante em cada partícula.

Depois do cálculo das forças, as posições e as velocidades das partículas são

atualizadas. Dado que os corpos têm novas posições, uma nova árvore deve ser formada e

todo o procedimento repetido para um número determinado de iterações.

Partícula Sub árvore Região Vazia

Figura 3: Árvore correspondente à divisão apresentada na figura 2.

Page 13: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída - 13

Segue o algoritmo da simulação seqüencial:

3.3.3 Simulação Paralela

Existem duas questões importantes que devem ser ressaltadas na implementação

da simulação paralela para o problema de N-corpos em relação a seqüencial:

• Particionar o problema em módulos e mapear efetivamente esses módulos aos

processadores: cada subdomínio é associado a um processador que é

responsável por todos os cálculos associados com as partículas que fazem parte

de seu domínio.

• Estrutura de comunicação que minimize o tamanho e a quantidade de

mensagens: na solução do problema é preciso conhecer a massa e a posição de

todas as outras partículas, portanto é necessário a troca de informação entre

os processadores.

O fato de o domínio físico ser dividido em dois subdomínios retangulares garante o

balanceamento de carga e assim provê uma estrutura de comunicação eficiente.

A divisão do domínio físico ocorre recursivamente até que um processador esteja

associado com cada subdomínio retangular. Na final do particionamento existem p

subdomínios, onde p é o número de processadores disponíveis.

Cada processador tem um domínio espacial e fazem divisões recursivas de seus

subdomínios representando suas partículas em forma de árvore binária e os cálculos de

Simulacao_sequencial()

Início

Inicializar_particulas(massa, posicao, velocidade)

Para cada iteração faça

Início

Construir_arvore_binaria( );

Calcular_para_cada_no(massa, centro_massa);

Para cada partícula faça

Início

Calcular_forca(no, particulas);

Atualizar(posicao, velocidade);

Fim_para

Fim_para

Fim.

Page 14: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída - 14

massa e centro de massa são realizados como na simulação seqüencial.

Os processadores trocam as informações necessárias para calcular a massa e o

centro de massa de todo domínio físico. As informações são passadas dos domínios dos

processadores (folhas) para a raiz onde ocorre o cálculo e então, repassada para todos os

processadores.

A árvore ORB é chamada Le-tree (Locally Essence Tree) quando associada com as

partículas locais do processador e com as informações relevantes das partículas dos

outros processadores. No final da execução do algoritmo le-tree os processadores

possuem as informações necessárias para o cálculo da força resultante das partículas de

seu domínio, que é feito pela mesma função utilizada na simulação seqüencial.

Cada processador deve checar se após a atualização dos valores da posição e

velocidade das partículas elas ainda pertencem ao seu domínio, se não, deve enviá-las ao

processador apropriado. A transferência de partículas provoca desequilíbrio de carga

sendo necessário um procedimento de balanceamento de carga capaz de designar novos

domínios (operação de transferência de tarefas) aos processadores tal que o número de

partículas associadas aos processadores esteja balanceado.

Segue o algoritmo da simulação paralela:

Simulacao_paralela()

Início

Inicializar_particulas(massa, posicao,velocidade);

Distribuir_particulas();

Para cada processador em cada iteração faça

Início

Trocar_centros_massas();

Construir_le_tree();

Para cada partícula faça

Início

Calcular_forca (raiz, particula);

Atualizar(posicao, velocidade);

Fim_para

Enviar_particulas_fujonas();

Receber_particulas_fujonas();

Invocar_balanceamento_carga();

Fim_para;

Fim.

Page 15: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída - 15

3.4 A Implementação do Problema

Neste trabalho foi implementada a simulação paralela time-stepped totalmente

concorrente para o problema de N-corpos. Para esta implementação foi utilizada a

linguagem de programação C e para efetuar a troca de mensagens entre os processadores

foi usado MPI.

Visto que o problema de N-corpos é um problema de física complexo, algumas

considerações foram assumidas para a simplificação do problema.

3.4.1 Considerações gerais

Distribuição das partículas

As partículas como no problema original estão espalhadas em uma área determinada.

O objetivo do programa é simular passo a passo o comportamento das partículas, isto é

como elas se movimentam dentro desta área. A movimentação das partículas é dada pela

sua velocidade e pela influência das massas de outras partículas.

Na implementação proposta, não é considerado o balanceamento de carga entre os

processos participantes na simulação. Isto foi definido, pois, a idéia principal é mostrar

como a simulação progride e não propor uma carga de trabalho igual para todos os

processos. Desta forma, a área considerada é dividida em partes iguais ao número de

processos, não importando o número de partículas existentes em cada área.

Considerando quatro processos e oito partículas distribuídas em uma área de

tamanho 40x80, no exemplo abaixo a distribuição entre os processos seria a seguinte:

Figura 4: Exemplo de distribuição de um conjunto de 8 partículas entre 4processos

(60,16)

(70,5)

(52,39)

(49,28)

Part1

(41,11)

Part3

(30,22)

(19,34)

Part7

(1,33)

Part0

39

790

Part5

Part6

Part2

Part4

Processo 0 Processo 1 Processo 2 Processo 3

Page 16: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída - 16

O processo 0 recebe as partículas 0 e 7, o processo 1 a partícula 5, o processo 2 as

partículas 1, 3 e 6 e o processo 3 as partículas 2 e 4. Cada processo fica responsável

apenas pelas partículas pertencentes a sua área, e em cada iteração terá calculado uma

nova posição para suas partículas. A nova posição dependerá diretamente da influência de

todas as outras partículas do sistema.

Apenas o processo 0 executa a inicialização e distribuição das partículas, ele é

responsável por verificar a posição de cada partícula e enviá-la ao processo

correspondente.

Além da posição (x,y) inicial, cada partícula possui massa, forca, velocidade e

direção. Tais valores são atribuídos aleatoriamente na fase de inicialização de partículas.

A força inicialmente é zero, e a direção é um atributo adaptado para que se possa calcular

uma nova posição para a partícula. Neste trabalho, uma partícula pode estar se

movimentando apenas em quatro direções, Norte, Sul, Leste e Oeste. Para completar o

exemplo que será usado, consideremos que cada partícula possui os seguintes dados:

Part0 Part1 Part2 Part3 Part4 Part5 Part6 Part7

Posição (1,33) (52,39) (70,5) (41,11) (60,16) (30,22) (49,28) (19,34)

Massa 52,0 91,0 61,0 99,0 70,0 8,0 78,0 49,0

Força 0 0 0 0 0 0 0 0

Velocidade 7,0 6,0 4,0 1,0 9,0 8,0 6,0 5,0

Direção Leste Sul Oeste Norte Sul Oeste Norte Leste

Criação da árvore (associação de partículas)

Como foi explicado na seção 2.3, para diminuir a complexidade do algoritmo, uma

partícula recebe influência ou de partículas individuais ou de grupos de partículas. Na

implementação apresentada as partículas são agrupadas de acordo com a distância entre

elas. Uma partícula sempre tenta se associar a partícula mais próxima a ela. Somente as

partículas pertencentes a um mesmo processo podem se associar. Desta forma é criada

uma árvore para cada processo.

O método para criação da árvore é guloso. Primeiro encontra-se a partícula mais a

esquerda e depois a partícula mais próxima a ela. Assim, estas partículas são colocadas em

Page 17: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída - 17

uma árvore, sendo filhas de um mesmo nó. Este nó guardará o centro de massa do grupo e

sua nova massa.

Grupos de partículas também são associados até que se resulte em apenas um grupo

por processo. Assim, no final, cada processo possuirá um grupo de partículas que terá seu

centro de massa e a massa total. Em outras palavras, cada processo possuirá sua árvore de

partículas que será usada para cálculo da força de cada partícula.

Seguindo o exemplo apresentado, cada um dos quatro processos criará seu grupo de

partículas como se segue:

Como é possível observar, por ser gulosa, a técnica implementada para a associação

das partículas pode nem sempre trazer resultados ótimos. Uma outra idéia que pode ser

usada é estabelecer uma distância mínima para se associar duas partículas.

Na figura abaixo, são mostradas as árvores de cada processo, e ao lado de cada nó

está o centro de massa e a massa total calculada:

Part1

Part6

Part7

Processo 0 Processo 1 Processo 2 Processo 3

Part3

Part0

Part5

Part2

Part4

Figura 5: Associação de partículas em grupos.

(64,65; 10,87)131,0

(9,73; 33,48)101,0

Processo 0 Processo 1 Processo 3

Part0 Part7 Part5 Part4 Part2

(47,06; 25,45)268,0

(44,52; 18,49)177,0 (52; 39)

91,0

Processo 2

Part1Part3 Part6

(30; 22)8,0

Figura 6: De acordo com a associação mostrada na figura 5, as seguintes árvores sãocriadas em cada processo.

Page 18: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída - 18

A massa total de um nó é calculada somando as massas individuais de todas as

partículas descendentes daquele nó. O centro de massa de um grupo deve ser calculado

considerando as massas das duas partículas i, j (ou dos dois grupos) que estão sendo

associadas e suas posições.

TotalMassa = Massai + Massaj

CentroMassax = (Massai * Posxi + Massaj * Posxj)/(Massai + Massaj)

CentroMassay = (Massai * Posyi + Massaj * Posyj)/(Massai + Massaj)

Cálculo da força das partículas

Cada processo calcula a força das partículas pertencentes a ele, com base nas

informações armazenadas em sua árvore. Além disso, deve se considerar também as

informações armazenadas nas raízes das árvores dos outros processos que devem ser

trocadas entre si. Logo, cada partícula recebe influência das partículas ou grupos de

partículas pertencentes ao seu processo e dos grupos de partículas formados nos outros

processos, considerando suas massas e distâncias aos centros de massa.

Neste trabalho a força que uma partícula i recebe em relação a uma partícula ou

grupo j é calculada dividindo a massa de j pela distância entre i e j:

Forçai = Massaj/(Dist(i,j)

A força de uma partícula é calculada percorrendo a árvore de baixo para cima,

recursivamente, até se chegar a raiz. Após este passo é que ser contabilizadas as

informações das raízes dos outros processos. Para exemplificar considere o cálculo da

força da partícula 3 pelo processo 2.

(47,06; 25,45)268,0

(44,52; 18,49)177,0 (52; 39)

91,0

Processo 2

Part1Part3 Part6

Nível 2

Nível 1

Nível 0

Figura 7: Cálculo da força da partícula 3.

Page 19: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída - 19

O cálculo começa pelo nível 2. A força da partícula 3 será calculada com a influência

da partícula 6, já que elas são filhas do mesmo nó.

Força3 = Massa6 /(Dist(3,6) = 4,15

Subindo da para o nível 1 (para o nó pai da partícula 3), deve se adicionar a influência

exercida pelo grupo de partículas representada pelo nó cinza (nó irmão).

Força3 = Força3 + Massano_irmao /(Dist(3,no_irmao) = 4,15 + 3,02 = 7,17

Partindo para o último nível (raiz) o cálculo prossegue em relação aos grupos de

partículas dos outros processos. A soma abaixo é repetida em relação a todos os outros

processos existentes (0, 1 e 3).

Força3 = Força3 + TotMassa0 /(Dist(3,CentroMassa0) = 7,17 + 2,62 = 9,79

Força3 = Força3 + TotMassa0 /(Dist(3,CentroMassa1) = 9,79 + 0,51 = 10,30

Força3 = Força3 + TotMassa0 /(Dist(3,CentroMassa3) = 10,3 + 5,53 = 15,83Ao final, a força da partícula 3 é de 15,83.

Atualização da posição das partículas

Para calcular o deslocamento de uma partícula a cada iteração, foi utilizada a

seguinte fórmula da física:

Deslocamento = (velocidade * tempo) + (aceleração * tempo2)/2

Nesta implementação da simulação time-stepped, o tempo T inicial é 1, e a cada

iteração ele é incrementado em uma unidade. A velocidade é um dado inicial da partícula,

logo é necessário apenas calcular a aceleração para se descobrir o deslocamento de uma

partícula em um dado instante T.

A aceleração é calculada pela seguinte fórmula:

Força = massa * aceleração

Onde a força já foi calculada e a massa é um dado inicial da partícula. Considerando

a partícula 3 em um tempo inicial igual a 1, a sua nova posição é calculada da seguinte

Page 20: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída - 20

forma:

Acel3 = Força3/massa3 = 15,83/99 = 0,16

Desloc3 = (velocidade3 * T) + (acel3 * T2)/2 = 1 + 0,16/2 = 1,08

Considerando que a partícula 3 se movimenta na direção norte, basta atualizar sua

coordenada y. Seguindo a representação da área mostrada na figura, deve se diminuir o

valor de posy3 do deslocamento calculado, logo a nova posição da partícula será (41, 10).

Depois que todas as partículas tiverem suas posições atualizadas, deve se fazer uma

“redistribuição” das partículas, visto que elas podem passar da área de um processo para

outro. A redistribuição é feita a partir das informações dadas por cada processo que

devem informar aos outros a identidade das partículas que fugiram da sua área. Em

seguida cada processo gera uma nova árvore, calcula a força de suas partículas e suas

novas posições. Isto se repete até que o tempo máximo estabelecido para a simulação seja

atingido.

3.4.2 O Algoritmo

No programa devem se inicializadas as variáveis de controle para a utilização do

MPI, onde my_rank indicará o número do processo que está executando.

Como pode ser observado apenas o processo 0 é responsável por inicializar e

distribuir as partículas. Os outros processos devem estar preparados para receber as

partículas enviadas por 0. Cada processo guardará as informações de suas partículas em

um vetor chamado myparticula.

A partir do recebimento de suas partículas cada processo entra na simulação que

terá um número de passos pré-estabelecido (Tmax). Como foi explicado na subseção

anterior, cada processo constrói sua árvore, calcula a força e atualiza a posição de cada

partícula. Os processos informam aos outros sobre as partículas que fugiram de sua área

ao mesmo passo que recebem dos outros as partículas que vieram para sua área.

Page 21: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída - 21

Em linhas gerais, a função principal do programa implementado pode ser visto a

seguir:

3. Simulação Event-Driven

3.1 Conservativa

No método de simulação distribuída conservativa event-driven é garantido que os

eventos serão processados em ordem crescente de tempo para todos os nós, ou seja, para

todos os processos que fazem parte da simulação.

int main()

{

MPI_Init(&argc, &argv);

MPI_Comm_rank(MPI_COMM_WORLD, &my_rank);

MPI_Comm_size(MPI_COMM_WORLD, &p);

if (my_rank == 0){

inicializar_particulas();

distribuir_particulas();

}else

receber_particulas();

while (tempo < Tmax) {

construir_arvore();

trocar_centros_massa();

calcular_forca();

atualizar_posicao();

enviar_particulas_fujonas();

receber_particulas_fujonas();

tempo++;

MPI_Barrier(MPI_COMM_WORLD);

}

MPI_Finalize();

return(0);

}

Page 22: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída - 22

O método apresentado requer isomorfismo entre os grafos G (do processo físico) e

o g e as arestas são FIFO. Este método só poderá ser utilizado quando a estrutura de g

for conhecida.

Um protocolo de sincronização conservativo não tolera erros de causalidade. A

propriedade de causalidade garante que, entre todas as mensagens futuras, nenhuma

possuirá uma marca de tempo menor que o valor do relógio local do processo. Então, se

uma sequência de K eventos é gerada nos tempos t1 < t2 < ... < tk ,por um processo físico vi

para acontecer em t1’, t2’,...,tk’ em vj , então também é exigido que t1’ < t2’ < ... < tk’.

Desta forma, toda vez que uma mensagem é recebida por um processo lógico sua

marca de tempo é comparada com o valor de seu relógio. Se esta marca de tempo for

maior então a mensagem é escalonada para seu próximo evento, caso contrário, se sua

marca de tempo for menor que o valor do relógio local do processo, a simulação é

interrompida.

Para o algoritmo, assume-se que g é um grafo direcionado e fortemente conectado, o

que significa que um processo é forçado a conhecer quais outros processos interagem com

ele próprio e a fonte da simulação está em N0.

A propriedade de monotonicidade, exigida pelo método, garante que, se um nó

processa menssagens do tipo event(t) recebidas em ordem crescente de tempo t, as

mensagens enviadas para um nó ni também são recebidas em ordem crescente de t . Para

que ni processe as mensagens event(t) em ordem cerscente de tempo, ni deve unir em uma

fila todas as mensagens recebidas e depois processar esta fila em ordem crescente de

tempo.

Na verdade, não é necessário manter uma fila para todas as mensagens recebidas

por ni, o que acontece e que cada nó ni possuirá uma variável do tipo vetor chamada

next_time, onde cada processo vizinho a ni (ou seja, para todo nj , tal que nj ∈ I_Neigi)

corresponde a uma posição do vetor. A variável é inicializada com 0 e durante a simulação

conterá o parâmetro t da próxima mensagem event(t) vinda de nj que deve ser processada.

Para ni continuar sua simulação, deve se buscar um nk ∈ I_Neigi tal que next_timeik <

next_timeij , para que seja processado. Contudo, antes do processamento, ele deve

esperar uma nova mensagem event de nk para que um novo mínimo seja estabelecido.

Nesta abordagem pode ocorrer que a mensagem esperada nunca seja recebida. O

Page 23: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída - 23

processo ficará bloqueado até que esta situação seja resolvida. Um exemplo deste

problema ocorre na inicialização da simulação, onde o nó ni não pode processar qualquer

mensagem event até receber uma mensagem de cada vizinho.

O impasse é resolvido através da utilização do conceito de mensagens nulas

(mensagens com função apenas de sincronização), que atualizam os tempos nas filas de

entradas dos processos.

Uma mensagem do tipo null(t) significa que um processo físico vi não envia nenhuma

mensagem a vj entre o valor corrente do relógio do canal e o tempo t. Isto é, qualquer

futura mensagem do processo lógico ni para nj terá o valor do componente tempo

excedendo t. Essas mensagens não possuem nenhuma correlação com o sistema físico e são

utilizadas para indicar a ausência de mensagens, ou seja, é reconhecida pela não emissão

de mensagens do tipo event neste instante t.

Mais especificamente, quando mensagens event(t) ou null(t) são processadas pelo

nó ni , uma mensagem null(t+1) é enviada para cada nó nj ∈ O_Neigi (conjuntos vizinhos de

ni) para quem o ni não tem que enviar uma mensagem do tipo event. Esta mensagem

null(t+1) tem por objetivo informar a nj que não será enviada por ni qualquer event(t’) tal

que t’ <= t+1.

Se o processo a ser simulado é tal que ni pode predizer que nenhum event(t’) será

enviado para nj tal que t’ <= t para t+ > t+1, então uma mensagem null(t+) é enviada

imediatamente. Esta mensagem pode beneficiar o processamento de nj, permitindo que ele

continue seu processamento sem esperar mensagens de ni até no tempo t+.

A diferença t+-t-1 é conhecida como lookahead. Pode-se definir lookahead como a

possibilidade de previsão do tempo em que um determinado evento da simulação irá

ocorrer.

A mensagem do tipo null(t) é enviada também quando uma mensagem event(t) é

recebida tal que t > T, sinalizando que o nó ni não enviará outra mensagem porque sua

simulação já terminou.

Para armazenar o número de mensagens null utilizadas tanto para lookahead quanto

para terminação o ni utiliza uma variável chamada last_timeij, onde em cada posição

conterá o valor de t da última mensagem null(t) enviada para um vizinho nj.

O algoritmo que realiza a estratégia de simulação conservativa orientada a eventos

Page 24: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída - 24

pode ser visualizado a seguir. Além das variáveis já descritas são utilizadas também:

• needs_eventij → inicializada com true e indica se ni deve receber uma

mensagem do seu vizinho nj para continuar a simulação.

• null_messageij → se needs_eventi

j = false indica se a mensagem correspondente

next_timeij é do tipo null.

Código utilizado para inicialização da simulação. Os eventos iniciais são criados

espontaneamente. Depois, a cada passo, quando o estado dos processos são atualizados,

novos eventos podem ser criados.

Algorithm A_Simulate_C;

Variables: τi =0;

statei = xi(0);

next_timeij= 0 para todo nj ∈ I_Neigi;

lookaheadij para todo nj ∈ O_Neigi;

last_timeij = 0 para todo nj ∈ O_Neigi;

needs_eventij = true para todo nj ∈ I_Neigi;

null_messageij para todo nj ∈ I_Neigi;

Xi;

ti;

Input: msgi = nil;

Action if ni ∈ N0:Xi (conjunto de nós aos quais os eventos serão enviados)

for nj ∈ Xi do begin ti > τi é o tempo do event a ser enviado para nj

if ti <= T then Envia event(ti) para nj;

if ti > T ou I_Neigi = { } then Envia null(T) para nj;

end; for nj ∉ Xi do begin last_timei

j= min{τi + lookaheadij, T}

Envia null(last_timeij)para nj

end;

Page 25: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída - 25

Os próximos blocos apresentam o tratamento de mensagens do tipo evento, isto é,

quando um processo recebe um evento destinado a ele, que deve ser executado. E como foi

explicado na descrição da técnica conservativa, existe também um bloco do algoritmo

responsável pelo tratamento de mensagens do tipo null..

Input: msg = event(t) tal que nj → ni;

Action when needs_eventij: needs_eventi

j =false;

null_messageij =false;

next_timeij =t;

if not needs_eventik ∀nk ∈ I_Neigi then begin

Let nk ∈ I_Neigi tal que next_timeik < = next_timeil ∀nl∈ I_Neigi

τi = next_timeik

if null_messageik then Xi = {} else begin atualiza statei

Xi (conjunto de nós para enviar event´s)

end;for nl ∈ Xi do

begin Let ti > τi ,tempo do event para nl if ti <= T then Envia event(ti) para nl;

else Envia null(T) para nl;

end; for nl ∉ Xi do begin if last_timeil < min{τi + lookaheadil, T} then begin

last_timeil = min{τi + lookaheadi

l, T};

Envia null(last_timeil)para nl

end; end;

needs_eventik =true;

end.

Page 26: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída - 26

Input: msg = null(t) tal que nj → ni;

Action when needs_eventij: needs_eventi

j =false;

null_messageij =true;

next_timeij =t;

if not needs_eventik ∀nk ∈ I_Neigi then begin

Let nk ∈ I_Neigi tal que next_timeik < = next_timeil∀nl ∈ I_Neigi

τi = next_timeik

if null_messageik then Xi = {} else begin atualiza statei

Xi (conjunto de nós para enviar event´s)

end;for nl ∈ Xi do

begin Let ti > τi ,tempo do event para nl if ti <= T then Envia event(ti) para nl;

else Envia null(T) para nl;

end; for nl ∉ Xi do begin if last_timeil < min{τi + lookaheadil, T} then begin

last_timeil = min{τi + lookaheadi

l, T};

Envia null(last_timeil)para nl

end; end ;

needs_eventik =true;

end.

Page 27: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída - 27

3.2 Otimista

Na simulação conservativa os eventos são processados apenas quando existe a

certeza que nenhuma outra mensagem, com tempo defasado em relação ao tempo da

simulação, será recebida. Uma desvantagem desse tipo de simulação é o fato de não

oferecerem uma completa escalabilidade. Devido a isto, foi proposta uma nova técnica de

simulação orientada a eventos chamada Otimista, onde a abordagem mais conhecida

refere-se ao algoritmo Time Warp.

No Time Warp os eventos são processados com a esperança de que nenhuma

mensagem com tempo de ocorrência defasado chegue ao processo. O tempo de simulação

para cada processo (LVT) é definido como o tempo de ocorrência do último evento

processado. Quando um processo recebe uma mensagem com seu tempo no passado

(defasada), o estado da simulação é retornado para o último estado consistente antes da

chegada desta mensagem. Um exemplo de envio de mensagem defasada entre processos

está representado na figura abaixo. Note, que o processo 0 envia uma mensagem ao

processo 1 com o tempo de ocorrência menor que o tempo local do processo 1.

Tempo

Processo 0 escalona umevento no “passado” doprocesso 1

Tempo Local

Processo 0

Processo 1

Figura 8: Processamento Otimista de Eventos. Representação do escalonamentode uma tarefa em um tempo passado da simulação.

Page 28: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída - 28

Para resolver o problema (viabilizar o rollback), um mecanismo de armazenamento

deve ser implementado para recuperar o estado de um processo da simulação.

Um dos conceitos mais importantes com relação a simulações otimistas diz respeito

ao Global Virtual Time (GVT). GVT é definido como o menor valor entre os LVT’s que

compõem a simulação distribuída. Normalmente não se deve permitir que os processos

escalonem novos eventos com marcas de tempos no passado, ou seja, eventos com marcas

de tempo menores que o GVT. Em outras palavras, os eventos com marcas de tempo

menores que a do GVT foram corretamente processados e nunca levarão a erros de

causalidade. Por esse motivo deve-se determinar sempre que possível o valor do GVT sem

prejudicar o processamento da simulação.

Outra razão para se realizar freqüentes atualizações no GVT é que existem

simulações onde a iteratividade é bem grande. Mensagens válidas de um evento podem ser

processadas seguramente quando o evento é confiável e para saber se um evento é

confiável deve-se saber o valor do GVT. Se o valor do GVT for calculado muito

freqüentemente em uma simulação interativa em tempo real, então o tempo de resposta

da interatividade pode ser menor.

Quando um evento é processado, ele pode gerar novos eventos. Estes novos

eventos são escalonados através de envios de mensagens. Quando um evento é retornado,

o processo da simulação deve ser reiniciado com seus valores originais e qualquer

mensagem gerada deve ser cancelada através do envio de suas correspondentes

antimensagens (ou antievento - isso significa que o Time Warp deve manter informações

referentes às mensagens que foram geradas por determinado evento).

As figuras abaixo mostram os passos envolvidos quando uma mensagem atrasada é

recebida e quando uma antimensagem é escalonada por um processo. O processo de

retornar ao evento da simulação que não deveria ter sido escalonado. Para corrigir este

erro o evento deve ser retirado da lista de eventos futuros da simulação. Caso este

evento tenha emitido erroneamente novas mensagens, estas mensagens também devem ser

anuladas através da emissão de novas antimensagens (podendo gerar uma explosão em

cascata de antimensagens).

Page 29: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída - 29

Mensagem Atrasada

Global Virtual Time

ProcessodaSimulação

Tempo da simulação

Último Evento Processado

P ó i E t P

Global VirtualTime

ProcessodaSimulação

Tempo da simulação

Eventos

antimensagens

Antimensagem

Global VirtualTime

ProcessodaSimulação Tempo da simulação

Último Evento

Evento Cancelado

Global VirtualTime

ProcessodaSimulação Tempo da simulação

Eventos Retornados

antimensagens

Figura 9: (a) RollBack no Time Warp. Representação gráfica do processo deemissão de mensagens durante o rollback. (b) Utilização deantimensagens no Time Warp

Page 30: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída - 30

A seguir, é mostrado um algoritmo genérico para representar este tipo de simulação:

Algorithm A_Simulate_TW;

Variables: Ti =0;

statei = xi(0);

state_queuei = nil;output_queuei = nilinput_queuei = nilti;

Input: msgi = nil;

Action if ni ∈ N0:Append (Ti, statei) to state_queuei;

for nj ∈ Neigi do if there exists event to be sent to nj then

begin Let ti > Ti be the event’s time to be sent to nj;

if ti <= Time then Send event(ti) to nj;

end.

Input: msgi = anti_event(t) such that origini(msgi) = (ni,nj);

Action: if t <= Ti then

beginLet t’ be the greatest integer such that t’ < t and there

exists (t’,x) in state_queuei;

statei := x;

Remove all (t+,x’) such that t+ >= t from state_queuei;

for all (t+,t’,nk) in output_queuei such that t+ >= t do begin

Remove (t+,t’,nk) from output_queuei;

Send anti_event(t’) to nk;

end;Ti := t;

end else

Add (t,nj) to input_queuei;

Page 31: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída - 31

3.3 O Problema de Filas

Para ilustrar a simulação orientada a eventos será utilizado um exemplo da própria

computação: a abordagem de um sistema operacional distribuído. O objetivo é simular

alguns eventos que ocorrem em sistema operacional, com a intenção de explorar melhor as

características de um sistema orientado a eventos que se encaixe em uma das técnicas

descritas nas subseções 3.1 e 3.2.

Em um sistema distribuído, os processadores podem possuir recursos e funções

diferentes. Cada processador normalmente possui seus próprios processos para executar,

Input: msgi = event(t) such that origini(msgi) = (ni,nj);

Action: if there exists (t,nj) in input_queuei then

Remove (t,nj) from input_queuei;

else begin

if t <= Ti then begin

Let t’ be the greatest integer such that t’ < t and

there exists (t’,x) in state_queuei;

statei := x;

Remove all (t+,x’) such that t+ >= t from state_queuei;

for all (t+,t’,nk) in output_queuei such that t+ >= t do begin

Remove (t+,t’,nk) from output_queuei;

Send anti_event(t’) to nk; end;

end; Ti := t;

Update statei;

Append (Ti, statei) to state_queuei;

for nk ∈ Neigi do if there exists event to be sent to nk then

begin Let ti > Ti be the time of the event to be sent to nk;

if ti <= Time thenbegin

Append (Ti,ti,nk) to output_queuei;

Send event(ti) to nk;end

end end.

Page 32: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída - 32

porém alguns processos podem precisar de recursos que pertencem a outros

processadores, como por exemplo, um serviço de impressão. Neste caso, deve se enviar um

pedido para o processador correspondente. A idéia do trabalho será simular estes pedidos

que serão disparados como eventos.

A política de escalonamento de pedidos utilizada será do tipo FIFO (First In First

Out), esta é uma política simples e em alguns casos eficiente, como o foco deste estudo

não é o sistema operacional e sim o comportamento dos eventos dentro do sistema esta

política se enquadra na nossa necessidade. Neste caso, o primeiro evento a entrar no

sistema, será o primeiro a ganhar o processador para ser executado e permanecerá

executando até a sua finalização.

Como cada processador pode disparar seus pedidos a qualquer momento, se um

evento E1 com instante de chegada igual a t1 é criado antes de um outro evento E2 com

instante de chegada igual a t2, então necessariamente t1 < t2. Com esta propriedade se

garante que os eventos serão criados e inseridos na fila de eventos em ordem de chegada

(inserção FIFO).

Quando um processo cria um evento com necessidade de execução em outro

processo, este evento é colocado na fila de eventos do primeiro e assim que possível o

evento é enviado para o processo que possui os recursos necessários para executá-lo,

como mostra a figura a seguir:

Seta para baixo – Chegada

do evento

Seta para cima – Saída do

evento (após ser tratado)

Figura 10: Exemplo de execução de eventos seguindo uma política FIFO.

Page 33: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída - 33

3.4 A Implementação do Problema

Para o problema das filas, foi utilizada a simulação orientada a eventos conservativa.

Tal escolha foi feita, pois os ambientes distribuídos estão sujeitos à falhas e a diferença

entre os relógios locais dos processadores também pode ser grande. Desta forma,

garantir que a simulação ocorrerá sem percalços na ordenação do tempo dos eventos não

seria uma boa política, logo não seria proveitoso fazer as suposições consideradas pela

implementação otimista.

Além disso, o sistema operacional utiliza dispositivos de E/S que não possuem a

propriedade de retornar a um estado anterior, como uma impressora, por exemplo, uma

vez feita a impressão, não há como desfazer esta tarefa (a utilização de antimensagens

não eliminaria uma impressão fora de hora) .

Como no problema apresentado na seção anterior, utilizou-se a linguagem de

programação C com MPI.

3.4.1 Considerações Gerais

Figura 11: Eventos devem ser encaixados nas filas dos processos com os recursosnecessários a sua execução. O evento em cinza deve sair de V1 e ir para V3.

Page 34: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída - 34

Assim como no problema de N-corpos, algumas simplificações para o problema de

filas foram adotadas, visto que o objetivo não é implementar um sistema de filas perfeito

e sim utilizar sua idéia para exemplificar uma simulação orientada a eventos.

O sistema físico

O sistema a será composto de diversos computadores (processos) e cada um terá

uma função específica de acordo com o tipo de recursos que ele possui:

• Servidor de Impressão – Gerência as impressoras do sistema

• Servidor de Arquivos – Gerência de todos os arquivos do sistema

• Servidor de Disco – Gerência o espaço e a ocupação dos diversos dispositivos

de armazenamento do sistema.

• Servidor de Cd-Rom – Gerência o acesso a discos óticos do sistema.

Na implementação do programa estes servidores (processos) serão identificados por

números inteiros (0, 1, 2, 3). Outra característica do importante é que os servidores são

totalmente conectados entre si.

Cada processo terá sua própria fila de eventos. Se os eventos de um processo

necessitarem de recursos de outros processos eles devem ser enviados para os processos

correspondentes.

Os eventos

Os eventos serão criados aleatoriamente nos diversos computadores (processos)

que irão compor o sistema, a necessidade de recursos para estes eventos também será

escolhida aleatoriamente. Para simplificar o problema, um evento só pode requisitar um

recurso do sistema (arquivo, impressão, etc), logo um evento muda de fila apenas uma vez.

Os eventos possuirão os seguintes dados:

• Identificador do evento

• Processo onde foi disparado

• Instante de Chegada do evento (Instante que o evento foi disparado, tal

instante é escolhido aleatoriamente, com a propriedade de que os eventos

Page 35: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída - 35

devem ser criados em uma ordem de tempo crescente)

• Tempo necessário para execução (Adota-se uma unidade de tempo para

todos os eventos)

• Identificador do pedido (Recurso necessário para a execução)

A execução

A simulação irá progredir até um tempo máximo pré-estabelecido. Durante a

execução, os processos envolvidos na simulação criam seus eventos dinamicamente. A

quantidade de eventos criados em um passo é escolhida randomicamente.

Um pseudocódigo bem simples pode ser dado como se segue abaixo:

1. Inicialização e cálculo do estado dos processos

2. Começa a criação (randômica) de eventos

3. Recebimento e envio de mensagens nulas ou contendo eventos, em um

tempo t

4. Simula a execução do próximo evento

5. Volta para o passo 3

Na simulação em questão os processos podem ter seus tempos locais diferentes,

além disso, eles podem criar e executar diferentes números de eventos. Desta maneira,

processos podem terminar sua execução em momentos distintos. Para que o programa

termine corretamente deve se implementar um pequeno algoritmo de terminação que não

permita que um processo fique esperando para receber uma mensagem de um outro

processo que já tenha terminado.

Cada processo possui um vetor com entrada para todos os seus vizinhos que indicam

se eles tenham terminado ou não. Se um processo A recebe alguma informação de um

processo B indicando que ele tenha terminado, A deve atualizar em seu vetor a entrada

correspondente ao processo B. Assim, A não envia mais mensagens para B e também não

espera mensagens dele. Porém, um processo só pode finalizar sua execução depois que o

vetor de terminação indique que todos os seus vizinhos já tenham terminado, evitando

terminações prematuras e possíveis deadlocks.

Page 36: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída - 36

Page 37: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída - 37

3.4.2 O Algoritmo

O algoritmo implementado pode ser resumido da seguinte forma:

int main()

{

int i, j , t_rec, fim = 0;

MPI_Init(&argc, &argv);

MPI_Comm_rank(MPI_COMM_WORLD, &my_rank);

MPI_Comm_size(MPI_COMM_WORLD, &p);

inicializar_campos_do processo();

cria_evento();

tratar_msg_nil();

while (!fim) {

for (i = 0; i < nmaxproc-1; i++) {

MPI_Recv(&t_rec, 1, MPI_INT,MPI_ANY_SOURCE,MPI_ANY_TAG,MPI_COMM_WORLD,&status);

If (status.MPI_TAG == 2) {

tartar_msg_null(t_rec);

if (t_rec == nulo) {

terminacao[status.MPI_SOURCE] = 1;

processo.needs_event[[status.MPI_SOURCE] = 0;

}

}else{

incluir_evento_fila(t_rec);

tratar_msg_evento(t_rec);

}

}

if (processo.tau >= Tmax)

terminacao[my_rank] = 1;

else

cria_evento();

fim = 1;

for (i = 0; i < nmaxproc; i++)

if (terminacao[i] == 0)

fim = 0;

}

MPI_Finalize();

return(0);

}

Page 38: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída - 38

4. Considerações Finais

O objetivo deste trabalho foi o de se estudar técnicas de simulação distribuída.

Para isso foram utilizados os problemas de N-corpos e o de gerenciamento de filas de

eventos. O problema de N-Corpos foi adaptado para a simulação time-stepped (orientada

por intervalos de tempo) enquanto que o gerenciador de filas foi usado para exemplificar a

simulação event-driven (orientada a eventos). Os algoritmos implementados foram

retirados de [1].

A implementação de problemas distribuídos se mostra bem complexa e os resultados

são sempre imprevisíveis, já que a cada execução as diferentes máquinas do sistema

podem estar se comportando de maneira diferente. Ou seja, mensagens podem ser

enviadas mais rapidamente ou não dependendo do congestionamento do sistema, o que

torna a depuração e correção dos programas mais difícil. Outro ponto importante é que na

programação distribuída os processos envolvidos no sistema não podem esperar por

mensagens indefinidamente, logo, a necessidade de algoritmos de terminação eficientes é

bem importante.

O estudo da simulação distribuída se mostrou muito interessante, e foi possível

perceber através deste trabalho, de palestras e reuniões durante o curso que tal técnica

pode ser usada tanto em problemas estritamente computacionais como e também em

vários problemas do cotidiano como uma simulação social ou até mesmo a simulação do

funcionamento do sistema físico humano.

5. Referências Bibliográficas

[1] An Introduction to Distributed Algorithms, Valmir C. Barbosa. The MIT Press,

1996

[2] Tese de N-corpos

[3] Manual de MPI

Page 39: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída - 39

Anexo A

A.1 Código fonte da simulação do problema de N-Corpos.#include <stdio.h>#include <stdlib.h>#include <string.h>#include <mpi.h>#include <unistd.h>#include <limits.h>#include <math.h>#include <time.h>#include <sys/ddi.h>#include <sys/types.h>

#define Tmax 7 // Maximo de interacoes do algoritmo#define nmaxpart 8 // Numero maximo de particulas#define nmaxproc 4 // Numero maximo de processos#define maxposx 80 // Maximo valor definido para o eixo x#define maxposy 40 // Maximo valor definido para o eixo y#define maxmassa 100 // Maxima massa#define maxveloc 10 // Maxima velocidade#define mindist 200 // Minima distancia para se unir duas particulas

//##############################################################################//############### Declaracao de tipos e variaveis ##########################//##############################################################################

typedef struct campospart { int idpart; int posx; int posy; int pai; int direcao; float velocidade; float massa; float forca; }campospart;

typedef struct campospartfujonas { campospart particula[nmaxpart]; int qtdpart; }campospartfujonas;

typedef struct camposproc { int posxini; int posxfim; int qtdpart; float totmassa; float centromassax; float centromassay; }camposproc;

typedef struct camposno { float totmassa; float centromassax; float centromassay; int filhopart; // boolean, indica se o no tem filhos particula int filhoesq; int filhodir; int pai; } camposno;

typedef struct tipoarvore { camposno no[nmaxpart*2]; int qtdnos; int raiz; } tipoarvore;

MPI_Datatype MPI_CAMPOS_PART;MPI_Datatype MPI_CAMPOS_PROC;

int tempo = 1;campospart particula[nmaxpart];campospart myparticula[nmaxpart];tipoarvore arvore;

Page 40: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída - 40

camposproc otherdata[nmaxproc-1];camposproc mydata;

int my_rank;int tag;int p=4;int message[];MPI_Status status;

//##############################################################################//########## Funcao que implementa a geracao de valores aleatorios ##########//##############################################################################void randomize(){ srand((unsigned int)time((time_t *)NULL));}

//Gera um numero aleatorio.unsigned int intRandom(const unsigned int maxValue) {

unsigned int res; static unsigned int rgen; static double factor; rgen = rand(); factor = ((double)rgen/(double)SHRT_MAX); res = (unsigned int) ( maxValue * factor ); if (res==maxValue) res--; return res;}

//##############################################################################//### Funcao que cria um tipo MPI estrututado para processo ###//##############################################################################void criar_tipo_MPI_processo(camposproc process){

int i;

MPI_Datatype type[6] = {MPI_INT, MPI_INT, MPI_INT, MPI_FLOAT, MPI_FLOAT, MPI_FLOAT};int tambloco[6] = {1, 1, 1, 1, 1, 1};MPI_Aint disp[6];int base;/* compute displacements of structure components */

//MPI_Address(process, disp);MPI_Address(&process.posxini, disp);MPI_Address(&process.posxfim, disp+1);MPI_Address(&process.qtdpart, disp+2);MPI_Address(&process.totmassa, disp+3);MPI_Address(&process.centromassax, disp+4);MPI_Address(&process.centromassay, disp+5);

base = disp[0];for (i=0; i <6; i++) disp[i] -= base;

MPI_Type_create_struct(6, tambloco, disp, type, &MPI_CAMPOS_PROC);

MPI_Type_commit(&MPI_CAMPOS_PROC);}

//##############################################################################//### Funcao que cria um tipo MPI estrututado para particula ###//##############################################################################void criar_tipo_MPI_particula(campospart particle[nmaxpart]){

int i;

MPI_Datatype type[8] = {MPI_INT, MPI_INT, MPI_INT, MPI_INT, MPI_INT, MPI_FLOAT, MPI_FLOAT,MPI_FLOAT};int tambloco[8] = {1, 1, 1, 1, 1, 1, 1, 1};MPI_Aint disp[8];int base;/* compute displacements of structure components */

MPI_Address(particle, disp);MPI_Address(&particle[0].idpart, disp);MPI_Address(&particle[0].posx, disp+1);MPI_Address(&particle[0].posy, disp+2);

Page 41: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída - 41

MPI_Address(&particle[0].pai, disp+3);MPI_Address(&particle[0].direcao, disp+4);MPI_Address(&particle[0].velocidade, disp+5);MPI_Address(&particle[0].massa, disp+6);MPI_Address(&particle[0].forca, disp+7);

base = disp[0];for (i=0; i <8; i++) disp[i] -= base;

MPI_Type_create_struct(8, tambloco, disp, type, &MPI_CAMPOS_PART);

MPI_Type_commit(&MPI_CAMPOS_PART);}

//##############################################################################//## Funcao que inicializa os atributos das particulas com valores aleatorios ##//##############################################################################void inicializar_particulas(){

int i; int t; for (i = 0; i< nmaxpart; i++) { srand(t); particula[i].massa = rand()%maxmassa; particula[i].posx = rand()%maxposx; particula[i].posy = rand()%maxposy; particula[i].velocidade = (rand()%maxveloc) + 1; particula[i].direcao = rand()%4; // 0-Norte 1-Leste 2-Sul 3-Oeste

particula[i].idpart = i; particula[i].forca = 0; t++; } printf("\n[%d] As particulas foram inicializadas\n", my_rank); fflush(stdout);}

//##############################################################################//## Funcao que distribui as particulas entre os processos. Apenas o processo ##//## zero faz a distribuicao e depois, envia para os outros processos um vetor #//## com as particulas correspondentes ao seu espaco. ##//##############################################################################void distribuir_particulas() {

int i, j, area; campospart auxparticula[nmaxpart]; camposproc auxproc;

area = maxposx / p; mydata.qtdpart = 0; for (i=0; i< p; i++){

auxproc.qtdpart = 0; auxproc.posxini = i * area; auxproc.posxfim = (i * area) + area - 1; for (j=0; j < nmaxpart; j++) { if ((particula[j].posx <= auxproc.posxfim) && (particula[j].posx >= auxproc.posxini)){

if (i == 0) { myparticula[mydata.qtdpart] = particula[j];

(mydata.qtdpart)++; }else{

auxparticula[auxproc.qtdpart] = particula[j]; (auxproc.qtdpart)++;

} }

} if (i != 0){ criar_tipo_MPI_processo(auxproc); criar_tipo_MPI_particula(auxparticula);

MPI_Send(&auxproc, 1, MPI_CAMPOS_PROC, i, 1, MPI_COMM_WORLD); MPI_Send(auxparticula, auxproc.qtdpart, MPI_CAMPOS_PART, i, 2, MPI_COMM_WORLD); }else{

mydata.posxini = auxproc.posxini; mydata.posxfim = auxproc.posxfim; } } printf("\n[%d] As particulas foram distribuidas para os processos\n", my_rank); fflush(stdout);

Page 42: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída - 42

}//##############################################################################//######### Funcao onde um processo recebe as particulas que fazem parte #######//######### da sua area. Apenas o processo zero distribui estas particulas. ####//##############################################################################void receber_particulas() {

int i; criar_tipo_MPI_processo(mydata); criar_tipo_MPI_particula(myparticula); MPI_Recv(&mydata, 1, MPI_CAMPOS_PROC, 0, 1, MPI_COMM_WORLD, &status); MPI_Recv(myparticula, nmaxpart, MPI_CAMPOS_PART, 0, 2, MPI_COMM_WORLD, &status);

printf("\n[%d] Recebimento das particulas\n", my_rank); fflush(stdout);}

//##############################################################################//## Funcao que calcula a distancia entre duas particulas ou dois aglomerados. #//##############################################################################float calcula_distancia(int posx1, int posy1, int posx2, int posy2){ float dist; dist = sqrt(((posx1 - posx2)*(posx1 - posx2)) + ((posy1-posy2)*(posy1-posy2))); //printf("\n====>XY1(%d, %d) XY2(%d, %d) -> Distancia %f", posx1, posy1, posx2, posy2,dist); return (dist);}

//##############################################################################//## Funcao que insere uma particula ou um aglomerado de particulas na arvore. #//##############################################################################void insere_no_arvore(int posicao, int noesq, int nodir, int ispart) { if (ispart == 1) { // se e particula

if (nodir == -1) { arvore.no[posicao].totmassa = myparticula[noesq].massa; arvore.no[posicao].centromassax = myparticula[noesq].posx; arvore.no[posicao].centromassay = myparticula[noesq].posy; } else { arvore.no[posicao].totmassa = myparticula[noesq].massa + myparticula[nodir].massa; arvore.no[posicao].centromassax = ((myparticula[noesq].massa *myparticula[noesq].posx) + (myparticula[nodir].massa *myparticula[nodir].posx))/(myparticula[noesq].massa + myparticula[nodir].massa); arvore.no[posicao].centromassay = ((myparticula[noesq].massa *myparticula[noesq].posy) + (myparticula[nodir].massa *myparticula[nodir].posy))/(myparticula[noesq].massa + myparticula[nodir].massa); } arvore.no[posicao].filhopart = 1; arvore.no[posicao].filhoesq = noesq; arvore.no[posicao].filhodir = nodir; myparticula[noesq].pai = posicao; myparticula[nodir].pai = posicao; } else { if (nodir == -1) { arvore.no[posicao].totmassa = arvore.no[noesq].totmassa; arvore.no[posicao].centromassax = arvore.no[noesq].centromassax; arvore.no[posicao].centromassay = arvore.no[noesq].centromassay; } else { arvore.no[posicao].totmassa = arvore.no[noesq].totmassa + arvore.no[nodir].totmassa; arvore.no[posicao].centromassax = ((arvore.no[noesq].totmassa *arvore.no[noesq].centromassax) + (arvore.no[nodir].totmassa *arvore.no[nodir].centromassax))/(arvore.no[noesq].totmassa + arvore.no[nodir].totmassa); arvore.no[posicao].centromassay = ((arvore.no[noesq].totmassa *arvore.no[noesq].centromassay) + (arvore.no[nodir].totmassa *arvore.no[nodir].centromassay))/(arvore.no[noesq].totmassa + arvore.no[nodir].totmassa); } arvore.no[posicao].filhopart = 0; arvore.no[posicao].filhoesq = noesq; arvore.no[posicao].filhodir = nodir; arvore.no[noesq].pai = posicao; arvore.no[nodir].pai = posicao; } arvore.qtdnos++;}

//##############################################################################//## Funcao que cria uma "arvore", que na verdade e implementada em forma do ###//## vetor <arvore.no>. A arvore e necessaria para o armazenamento dos centros #

Page 43: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída - 43

//## de massa e depois para o calculo da forca exercida em uma particula. ###//##############################################################################void construir_arvore() {

int i, j, k, //Variáveis auxiliares par, //Particula mais próxima proximo,//Próximo nó da arvore vazio inivel, fnivel, auxnivel, // Indica as posicoes de inicio e fim de nivel particula_par[mydata.qtdpart*2]; //Particulas que já tem parfloat dist, menor;

for (i = 0; i < nmaxpart; i++) particula_par[i] = 0;

proximo = 0; inivel = 0;

for (k = 0; k < mydata.qtdpart; k++) {

//Acho o elemento mais a esquerdamenor = maxposx+1;for (j = 0; j < mydata.qtdpart; j++) {

if ((particula_par[j] == 0) && (myparticula[j].posx < menor)) {i = j;menor = myparticula[j].posx;

} }

menor = maxposx+1;

if (particula_par[i] == 0) { menor = maxposx+1;

par = -1; particula_par[i] = 1; //Acho o elemento mais perto da particula i

for (j = 0; j < mydata.qtdpart; j++) { dist = calcula_distancia(myparticula[j].posx, myparticula[j].posy,

myparticula[i].posx, myparticula[i].posy); if ((particula_par[j] == 0) && (dist < menor) && (dist < mindist)) {

par = j; menor = dist;

} } //Insiro o par na arvore

if (par != -1){ particula_par[par] = 1; insere_no_arvore(proximo, i, par, 1);

} else insere_no_arvore(proximo, i, -1, 1); proximo++;}

}

//Construção dos demais níveis da árvore inivel = 0; fnivel = proximo; while (fnivel - inivel > 1) { auxnivel = proximo; for (i = inivel; i < fnivel; i++) particula_par[i] = 0;

for (k = inivel; k < fnivel; k++) {

//Acho o elemento mais a esquerda menor = maxposx+1; for (j = inivel; j < fnivel; j++) {

if ((particula_par[j] == 0) && (arvore.no[j].centromassax < menor)) {i = j;menor = arvore.no[j].centromassax;

} }

menor = maxposx+1;

if (particula_par[i] == 0) { menor = maxposx+1;

par = -1; particula_par[i] = 1;

for (j = inivel; j < fnivel; j++) {

Page 44: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída - 44

dist = calcula_distancia(arvore.no[i].centromassax, arvore.no[i].centromassay,arvore.no[j].centromassax, arvore.no[j].centromassay);

if ((particula_par[j] == 0) && (dist < menor) && (dist < mindist)) { par = j; menor = dist; }

} //Insiro o par na arvore

if (par != -1){ particula_par[par] = 1; insere_no_arvore(proximo, i, par, 0);

} else insere_no_arvore(proximo, i, -1, 0);proximo++;

} } fnivel = proximo; inivel = auxnivel; } // fim do while

if (fnivel - inivel == 1) arvore.raiz = inivel;

printf("\n[%d] Árvore construida!\n", my_rank); fflush(stdout);}

//##############################################################################//######### Funcao que troca os centros de massa entre os processos. #########//##############################################################################void trocar_centros_massa() {

int i;camposproc auxotherdata;

if (mydata.qtdpart > 0){ mydata.totmassa = arvore.no[arvore.raiz].totmassa; mydata.centromassax = arvore.no[arvore.raiz].centromassax; mydata.centromassay = arvore.no[arvore.raiz].centromassay; } else {

mydata.totmassa = -1; mydata.centromassax = -1; mydata.centromassay = -1; } for (i = 0; i < p; i++) { if (i != my_rank) { criar_tipo_MPI_processo(mydata); MPI_Send(&mydata, 1, MPI_CAMPOS_PROC, i, 3, MPI_COMM_WORLD); //printf("\n(%d) Envio de TOTMASSA %f para %d", my_rank, mydata.totmassa, i);

} } for (i = 0; i < p; i++) { if (i != my_rank) { criar_tipo_MPI_processo(auxotherdata); MPI_Recv(&auxotherdata, 1, MPI_CAMPOS_PROC, i, 3, MPI_COMM_WORLD, &status);

otherdata[i].posxini = auxotherdata.posxini; otherdata[i].posxfim = auxotherdata.posxfim;

otherdata[i].qtdpart = auxotherdata.qtdpart; otherdata[i].totmassa = auxotherdata.totmassa; otherdata[i].centromassax = auxotherdata.centromassax; otherdata[i].centromassay = auxotherdata.centromassay; } } printf("\n[%d] Troca de centros de massa\n", my_rank); fflush(stdout);}

//##############################################################################//############## Funcao que calcula a forca de cada particula. ###############//##############################################################################void calcular_forca() {

campospart partforca[nmaxpart];int i, j, pai, avo;

for (i = 0; i < mydata.qtdpart; i++) { pai = myparticula[i].pai;

Page 45: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída - 45

if (arvore.no[pai].filhoesq == i){ if (arvore.no[pai].filhodir != -1) myparticula[i].forca =myparticula[arvore.no[pai].filhodir].massa/calcula_distancia(myparticula[arvore.no[pai].filhodir].posx, myparticula[arvore.no[pai].filhodir].posy, myparticula[i].posx, myparticula[i].posy);

else myparticula[i].forca = 0;

}else myparticula[i].forca = myparticula[arvore.no[pai].filhoesq].massa/

calcula_distancia(myparticula[arvore.no[pai].filhoesq].posx,myparticula[arvore.no[pai].filhoesq].posy, myparticula[i].posx,myparticula[i].posy);

while (pai != arvore.raiz) { avo = arvore.no[pai].pai; if (arvore.no[avo].filhoesq == pai){

if (arvore.no[avo].filhodir != -1) myparticula[i].forca = myparticula[i].forca +arvore.no[arvore.no[avo].filhodir].totmassa /calcula_distancia(arvore.no[arvore.no[avo].filhodir].centromassax,arvore.no[arvore.no[avo].filhodir].centromassay, myparticula[i].posx, myparticula[i].posy); }else

myparticula[i].forca = myparticula[i].forca +arvore.no[arvore.no[avo].filhoesq].totmassa /calcula_distancia(arvore.no[arvore.no[avo].filhoesq].centromassax,arvore.no[arvore.no[avo].filhoesq].centromassay, myparticula[i].posx, myparticula[i].posy);

pai = avo; } // fim do while

for (j = 0; j < nmaxproc; j++){ if ((j != my_rank) && (otherdata[j].totmassa != -1))

myparticula[i].forca = myparticula[i].forca + otherdata[j].totmassa /calcula_distancia(otherdata[j].centromassax, otherdata[j].centromassay, myparticula[i].posx,myparticula[i].posy); } } // fim do for}

//##############################################################################//######### Funcao que envia as particulas que sairam da area de um ########//######### processo para o novo processo ao qual ela pertence. ##############//##############################################################################void enviar_particulas_fujonas() {

int i, j, quantfujonas; campospart partfujonas[nmaxpart];

quantfujonas = 0; for (i = 0; i < mydata.qtdpart; i++){ if ((myparticula[i].posx < mydata.posxini) || (myparticula[i].posx > mydata.posxfim)){ partfujonas[quantfujonas] = myparticula[i]; quantfujonas++; mydata.qtdpart--;

for (j = i; j < mydata.qtdpart; j++)myparticula[j] = myparticula[j+1];

i = i - 1;}

} for (i = 0; i < p; i++) { if (i != my_rank) { MPI_Send(&quantfujonas, 1, MPI_INT, i, 4, MPI_COMM_WORLD); if (quantfujonas != 0) { criar_tipo_MPI_particula(partfujonas); MPI_Send(&partfujonas, quantfujonas, MPI_CAMPOS_PART, i, 5, MPI_COMM_WORLD); } } } fflush(stdout);}

//##############################################################################//######### Funcao que recebe as novas particulas que passam a fazer ########//######### parte da area de um processo. ##############//##############################################################################void receber_particulas_fujonas() {

int i, j, quantfujonas, cont = 0; campospart partfujonas[nmaxpart];

Page 46: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída - 46

for (i = 0; i < p; i++) { if (i != my_rank) { MPI_Recv(&quantfujonas, 1, MPI_INT, i, 4, MPI_COMM_WORLD, &status);

if (quantfujonas != 0) { criar_tipo_MPI_particula(partfujonas); MPI_Recv(&partfujonas, nmaxpart, MPI_CAMPOS_PART, i, 5, MPI_COMM_WORLD, &status); for (j = 0; j < quantfujonas; j++) { if ((partfujonas[j].posx >= mydata.posxini) && (partfujonas[j].posx <=mydata.posxfim)) {

myparticula[mydata.qtdpart] = partfujonas[j]; mydata.qtdpart++; } } } } } printf("\n[%d] Troca do vetor com as particulas fujonas\n", my_rank); fflush(stdout);}

//##############################################################################//######### Funcao que atualiza a posicao de um processo a partir de sua #######//######### forca, massa, velocidade e direcao. #######//##############################################################################void atualizar(){

int i, auxdesloc; float acel, desloc;

for (i = 0; i < mydata.qtdpart; i++) { acel = myparticula[i].forca/myparticula[i].massa; desloc = (myparticula[i].velocidade * tempo) + (acel * tempo * tempo)/2; auxdesloc = (int)desloc; if (myparticula[i].direcao == 2) { myparticula[i].posy = myparticula[i].posy + auxdesloc; if (myparticula[i].posy >= maxposy){ myparticula[i].posy = maxposy-1; myparticula[i].direcao = 0; } } else { if (myparticula[i].direcao == 1) { myparticula[i].posx = myparticula[i].posx + auxdesloc; if (myparticula[i].posx >= maxposx){ myparticula[i].posx = maxposx-1; myparticula[i].direcao = 3; } } else { if (myparticula[i].direcao == 0) { myparticula[i].posy = myparticula[i].posy - auxdesloc; if (myparticula[i].posy < 0){ myparticula[i].posy = 0; myparticula[i].direcao = 2; } } else { if (myparticula[i].direcao == 3) { myparticula[i].posx = myparticula[i].posx - auxdesloc;

if (myparticula[i].posx < 0){ myparticula[i].posx = 0; myparticula[i].direcao = 1; } } }

} } }}

//##############################################################################//########################### PROGRAMA PRINCIPAL ###############################//##############################################################################int main(int argc, char** argv) {

MPI_Init(&argc, &argv); MPI_Comm_rank(MPI_COMM_WORLD, &my_rank); MPI_Comm_size(MPI_COMM_WORLD, &p); if (my_rank == 0) {

Page 47: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída - 47

printf("\n\n\n ***** SIMULACAO TIME-STEPPED PARA O PROBLEMA DE N-CORPOS ***** \n\n\n"); printf("\n Numero de processos: %d\n", p); printf("\n Numero de particulas: %d\n\n\n", nmaxpart); inicializar_particulas(); distribuir_particulas(); MPI_Barrier(MPI_COMM_WORLD); } else { receber_particulas(); MPI_Barrier(MPI_COMM_WORLD); }

while (tempo < Tmax) { construir_arvore(); trocar_centros_massa(); calcular_forca(); atualizar(); enviar_particulas_fujonas(); receber_particulas_fujonas(); tempo++; MPI_Barrier(MPI_COMM_WORLD); }

printf("\n\n[%d] Fim da execucao!\n", my_rank); fflush(stdout); MPI_Finalize(); return(0);}

Page 48: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída - 48

A.2 Código fonte da simulação do problema de Filas.#include <stdio.h>#include <stdlib.h>#include <string.h>#include <mpi.h>#include <unistd.h>#include <limits.h>#include <math.h>#include <time.h>#include <sys/ddi.h>#include <sys/types.h>

//Declaracao de constantes.#define Tmax 8 // Tempo maximo da simulação#define intervalo 2 // Intervalo maximo de tempo entre dois eventos#define nmaxproc 4 // Numero maximo de processos#define impressao 0#define arquivo 1#define cdrom 2#define disco 3

//##############################################################################//##################### Declaracao de tipos e variáveis ####################//##############################################################################

typedef struct t_noevento { int tempo; int pedido; int fonte; struct t_noevento *next;} noevento;

typedef struct camposproc { int state; int next_time[nmaxproc]; int lookahead[nmaxproc]; int last_time[nmaxproc]; int needs_event[nmaxproc]; int null_message[nmaxproc]; int tau; int t; int quant_evento; noevento *evento; }camposproc;

int tempo = 1;int nulo = Tmax;int ultimoevento = 0;int terminacao[nmaxproc];camposproc processo;

int my_rank;int tag;int p=4;MPI_Status status;

//##############################################################################//## Funcao que insere eventos na fila ##//##############################################################################void incluir_evento_fila(int t){ noevento *aux, *pre;

aux = malloc(sizeof(noevento)); pre = processo.evento; aux->fonte = status.MPI_SOURCE; aux->pedido = my_rank; aux->tempo = t; if ((processo.evento == NULL) || (aux->tempo < processo.evento->tempo)) { aux->next = processo.evento; processo.evento = aux; } else { pre = processo.evento; while ((pre->next != NULL) && (pre->next->tempo < aux->tempo)) pre = pre->next; aux->next = pre->next;

Page 49: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída - 49

pre->next = aux; } processo.quant_evento++;}

//##############################################################################//## Funcao que apaga eventos da lista do processo ##//##############################################################################void apagar_evento(){ noevento *aux;

aux = processo.evento; processo.evento = processo.evento->next; free(aux);}

//##############################################################################//## Funcao que executa os eventos da fila ##//##############################################################################void executar_evento(int t){int i; while ((processo.evento != NULL) && (processo.evento->tempo <= t)){ printf("\n[%d] Executando o evento de [%d] no tempo %d\n", my_rank, processo.evento->fonte, processo.evento->tempo); apagar_evento(); } processo.tau = t; for (i = 0; i < nmaxproc; i++) if (processo.next_time[i] <= t) processo.needs_event[i] = 1;}

//##############################################################################//## Funcao que cria eventos aleatoriamente ##//##############################################################################void criar_evento(int t){ noevento *aux, *pre;

aux = malloc(sizeof(noevento)); srand48(time(NULL)+t); aux->pedido = (lrand48())%4; aux->fonte = my_rank;

pre = processo.evento; if (pre != NULL) { while (pre->next != NULL) pre = pre->next; ultimoevento = pre->tempo; aux->tempo = pre->tempo + processo.quant_evento + 1; }else aux->tempo = processo.tau + processo.quant_evento + 1;

if ((processo.evento == NULL) || (aux->tempo < processo.evento->tempo)) { aux->next = processo.evento; processo.evento = aux; } else {

pre = processo.evento; while ((pre->next != NULL) && (pre->next->tempo < aux->tempo)) pre = pre->next; aux->next = pre->next; pre->next = aux; } processo.quant_evento++;

}

//##############################################################################//## Funcao que imprime a lista de eventos de um processo ##//##############################################################################void imprimir_lista_evento(){ noevento *aux;

Page 50: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída - 50

aux = processo.evento; printf("\n\n Lista dos %d eventos do processo %d\n", processo.quant_evento, my_rank); while (aux != NULL) { printf("%d (%d) -> ",aux->pedido, aux->tempo); aux = aux->next; }}

//##############################################################################//## Funcao que inicializa os atributos dos processos ##//##############################################################################void inicializar_campos_processo(){ int j; for (j = 0; j< nmaxproc; j++) terminacao[j] = 0; processo.state = 0; for (j = 0; j< nmaxproc; j++) processo.next_time[j] = 0; for (j = 0; j< nmaxproc; j++) processo.lookahead[j] = 0; for (j = 0; j< nmaxproc; j++) processo.last_time[j] = 0; for (j = 0; j< nmaxproc; j++) processo.needs_event[j] = 1; for (j = 0; j< nmaxproc; j++) processo.null_message[j] = 0;

processo.tau = 0; processo.evento = NULL; processo.quant_evento = 0; criar_evento(processo.tau);

printf("\n[%d] Os eventos foram criados\n", my_rank); fflush(stdout);}

//##############################################################################//## Funcao que inicializa o processamento ##//##############################################################################void tratar_msg_nil(){

int j, t_aux, p_aux, aux, *buf, bufsize;

// Envia lasttime para os processos diferentes do pedido for (j=0; j< nmaxproc; j++) { if ((j != my_rank) && (j !=processo.evento->pedido)) { if (processo.tau + 1 + processo.last_time[j] < Tmax) aux = processo.tau + 1 + processo.last_time[j]; else aux = Tmax; processo.last_time[j] = aux; MPI_Send(&processo.last_time[j], 1, MPI_INT, j, 2, MPI_COMM_WORLD); } }

t_aux = processo.evento->tempo; p_aux = processo.evento->pedido;

// Envia evento para o processo pedido if ((p_aux != my_rank) && (t_aux <= Tmax)) {

MPI_Send(&t_aux, 1, MPI_INT, p_aux, 1, MPI_COMM_WORLD); apagar_evento(); }

// Envia nulo para o processo pedido se extrapolou o tempo if ((p_aux != my_rank) && (t_aux > Tmax)) { MPI_Send(&nulo, 1, MPI_INT, p_aux, 2, MPI_COMM_WORLD); apagar_evento(); }

}

Page 51: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída - 51

//##############################################################################//## Funcao que trata o recebimento de eventos ##//##############################################################################void tratar_msg_evento(int t_rec){

int j, k, l, t_menor, k_menor, t_aux, p_aux; int min, need = 0;

if (processo.needs_event[status.MPI_SOURCE] == 1){ processo.needs_event[status.MPI_SOURCE] = 0; processo.null_message[status.MPI_SOURCE] = 0; processo.next_time[status.MPI_SOURCE] = t_rec;

for (k = 0; k < nmaxproc; k++) if ((k != my_rank) && (processo.needs_event[k] == 1)) need = 1;

if (! need) { t_menor = Tmax*2; for (k = 0; k < nmaxproc; k++) { if (k != my_rank) { if (t_menor > processo.next_time[k]) { t_menor = processo.next_time[k]; k_menor = k; } } }

k = k_menor; //Executando o evento (considerando a execucao instantanea) executar_evento(t_menor);

//Envia lasttime para os processos diferentes do pedido (ou para todos se nao existepedido) for (l=0; l< nmaxproc; l++) { if (l != my_rank) { min = (processo.tau + 1) < Tmax ? (processo.tau + 1) : Tmax; if (processo.last_time[l] < min){ processo.last_time[l] = min; if (processo.evento == NULL) { MPI_Send(&processo.last_time[l], 1, MPI_INT, l, 2, MPI_COMM_WORLD); }else{ if (l !=processo.evento->pedido){ MPI_Send(&processo.last_time[l], 1, MPI_INT, l, 2, MPI_COMM_WORLD); }

} } } }

//Se existe pedido envia o evento para o processo do pedido if (processo.evento != NULL){ t_aux = processo.evento->tempo; p_aux = processo.evento->pedido; if ((my_rank != p_aux) && (t_aux <= Tmax) ) { MPI_Send(&t_aux, 1, MPI_INT, p_aux, 1, MPI_COMM_WORLD); apagar_evento(); } if ((my_rank != p_aux) && (t_aux > Tmax) ) { MPI_Send(&nulo, 1, MPI_INT, p_aux, 2, MPI_COMM_WORLD); apagar_evento(); } }

} }}

//##############################################################################//## Funcao que trata o recebimento de nulls ##//##############################################################################void tratar_msg_null(int t_rec){

int j, k, l, t_menor, k_menor, t_aux, p_aux; int min, need = 0;

Page 52: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída - 52

if (processo.needs_event[status.MPI_SOURCE] == 1){ processo.needs_event[status.MPI_SOURCE] = 0; processo.null_message[status.MPI_SOURCE] = 1; processo.next_time[status.MPI_SOURCE] = t_rec;

for (k = 0; k < nmaxproc; k++) if ((k != my_rank) && (processo.needs_event[k] == 1)) need = 1;

if (! need) { t_menor = Tmax*2; for (k = 0; k < nmaxproc; k++) { if (k != my_rank) { if (t_menor > processo.next_time[k]) { t_menor = processo.next_time[k]; k_menor = k; } } }

k = k_menor;

//Executando o evento (considerando a execucao instantanea) executar_evento(t_menor);

//Envia lasttime para os processos diferentes do pedido (ou para todos se nao existepedido) for (l=0; l< nmaxproc; l++) { if (l != my_rank) { min = (processo.tau + 1) < Tmax ? (processo.tau + 1) : Tmax; if (processo.last_time[l] < min){ processo.last_time[l] = min; if (processo.evento == NULL){ MPI_Send(&processo.last_time[l], 1, MPI_INT, l, 2, MPI_COMM_WORLD); }else{ if (l !=processo.evento->pedido){ MPI_Send(&processo.last_time[l], 1, MPI_INT, l, 2, MPI_COMM_WORLD); } } } } }

//Se existe evento envia o pedido para o processo correspondente if (processo.evento != NULL){ t_aux = processo.evento->tempo; p_aux = processo.evento->pedido; if ((my_rank != p_aux) && (t_aux <= Tmax) ) { MPI_Send(&t_aux, 1, MPI_INT, p_aux, 1, MPI_COMM_WORLD); apagar_evento(); } if ((my_rank != p_aux) && (t_aux > Tmax) ) { MPI_Send(&nulo, 1, MPI_INT, p_aux, 2, MPI_COMM_WORLD); apagar_evento(); } }

} }}

/************************** Programa Principal ********************************/

int main(int argc, char** argv){

int i, j, t_aux, t_rec, fim = 0;

//Inicializacao do MPI. MPI_Init(&argc, &argv); MPI_Comm_rank(MPI_COMM_WORLD, &my_rank); MPI_Comm_size(MPI_COMM_WORLD, &p);

inicializar_campos_processo(); srand48(time(NULL)+processo.tau);

Page 53: Técnicas de Simulação Distribuída

Técnicas de Simulação Distribuída - 53

criar_evento(processo.tau); t_aux = lrand48()%4; for (i=0; i < 1; i++) { if (ultimoevento < Tmax) { criar_evento(processo.tau+i); } }

tratar_msg_nil(); while (!fim) {

for (i = 0; i < nmaxproc; i++) { if(i != my_rank){ MPI_Recv(&t_rec, 1, MPI_INT, i, MPI_ANY_TAG, MPI_COMM_WORLD, &status); if (status.MPI_TAG == 2) { tratar_msg_null(t_rec); if (t_rec == nulo){ terminacao[status.MPI_SOURCE] = 1; processo.needs_event[status.MPI_SOURCE] = 0; } }else{ incluir_evento_fila(t_rec); tratar_msg_evento(t_rec); } } }

if (processo.tau >= Tmax){ terminacao[my_rank] = 1; printf("[%d] atualizei terminacao \n\n", my_rank); fflush(stdout);

}else{ srand48(time(NULL)+processo.tau); t_aux = lrand48()%2; for (i=0; i < t_aux; i++) { if (ultimoevento < Tmax) { criar_evento(processo.tau+i); printf("[%d] TAU: %i \n\n", my_rank, processo.tau); fflush(stdout); } } }

fim = 1; for (i = 0; i < nmaxproc; i++) { if (terminacao[i] == 0) fim = 0; } }

// Criterio de parada printf("\n\n[%d] Fim da execucao!\n", my_rank); fflush(stdout); MPI_Finalize(); return(0);}