• Nenhum resultado encontrado

7.1 Projeto do A-Star Paralelo

7.1.1 Algoritmo PA-Star

O PA-Star é dividido em 2 fases alternadas executadas em paralelo: search_step e verify_end_condition. A etapa search_step implementa a etapa de busca e expan- são do algoritmo A-Star (Seção 2.3.2.1, Algoritmo 2), das linhas 7 a 16 e a etapa ve- rify_end_condition implementa a linha 6 deste algoritmo.

O PA-Star é executado da seguinte forma: inicialmente, a thread t0 executa as linhas1

a4(Algoritmo 8). A função de hash H é aplicada ao nó inicial Ni e o id h é obtido (linha

1). O nó inicial Ni é colocado na OpenList da thread h (linha 2). A condição de parada

end_condition e o valor de custo f até o nó final n são inicializados como falso e ∞, respectivamente (linhas 3 e 4). Depois, t threads executam as duas etapas do algoritmo em paralelo (linhas 5a 9).

O search_step do PA-Star é descrito no Algoritmo9. Todas as threads executam uma versão modificada do algoritmo A-Star. Enquanto o contador de threads, threads_counter, é menor que o número total de threads, (linha 3), a thread ti consome a fila qi, removendo

Algoritmo 8 PA-Star(Ni, Nf)

1: h ← hash(Ni) /* Obtém a thread de destino */

2: OpenListh ← Ni /* Adiciona o nó inicial à OpenList */

3: end_condition ← f alse

4: n ← ∞ 5: repeat

6: /* Em paralelo i = 1 a t */

7: n ← search_step(i, Nf) /* Executa a etapa de busca para thread i */

8: end_condition ← verif y_end_condition(i) /* Condição de parada */

9: until end_condition == false 10: return n

Algoritmo 9 search_step(i, Nf)

1: threads_counter ← 0

2: f inal_node ← ∞

3: while threads_counter < threads_num do 4: /* Início da etapa de busca */

5: consume_queue(qi) /* Insere todos os nós da fila qi na OpenListi */

6: current ← OpenListi.get_lowest_f () /* Remove o nó de melhor prioridade da

OpenListi */

7: if current.g ≤ ClosedListi.f ind_g(current) then

8: /* Se um nó de melhor prioridade ainda não foi encontrado */

9: ClosedListi ← ClosedListi∪ current /* Adiciona o nó na ClosedListi */

10: if current ∈ Nf then

11: /* Se o nó é um nó final*/

12: threads_counter + + /* Aumenta os contadores de threads */

13: f inal_node ← current /* Salva o nó final */

14: process_f inal_node(current, threads_counter) /* Verifica condição de pa- rada */

15: else

16: for neighbor in neigh(current) /* Fase de expansão */ do

17: h ← hash(neighbor) /* Obtém a thread destino */

18: qh ← qh∪ neighbor /* Adiciona o nó na fila da thread h */

19: end for

20: end if

21: end if

22: /* Fim da etapa de busca */ 23: end while

todos os nós da qi e os adicionando à OpenListi (linha 5). Depois disso, o nó com menor

prioridade f é removido da OpenListi (linha6).

Se o nó removido da OpenListi não é um nó final, ele é adicionado à ClosedListi (linha

9) e expandido (linhas 16 a 19). Durante a fase de expansão, a função de hash sensível à localidade é utilizada para determinar em qual lista o nó vizinho deve ser adicionado (linha 17). Nós que pertencem à thread th são inseridos na fila qh (linha 18).

Quando o nó expandido é final (Nf) (linha 10), este nó só deve ser considerado o

resultado ótimo se tiver a menor prioridade f entre todos os nós das OpenLists de todas as threads. Essa verificação foi otimizada de maneira a dividir a verificação da condição de parada em duas fases (assíncrona e síncrona), procurando diminuir o overhead de comunicação das threads. Na fase assíncrona, o contador de threads threads_counter, é incrementado e a função process_final_node (linha 14) é chamada. Nesta função, se o valor de f do nó final é o menor entre todos os nós da thread, o nó Nf é inserido em todas

as OpenLists e o nó Nf é salvo em uma variável global.

Todas as threads continuam executando a etapa de busca search_step e quando a thread tiexpande o nó Nf de novo, isso significa que o nó Nf tem o menor valor de f entre

todos os nós da OpenListi e então o contador threads_counter é incrementado (linha

12). Quando threads_counter é igual a threads_num, todas as threads expandiram o nó Nf, então este nó é retornado como uma possível resposta (linha 24). Porém, outros

nós com menor prioridade f podem existir em outras filas qi ou podem ter sido inseridos

em alguma OpenList depois que a thread expandiu o nó Nf. Para garantir que o nó Nf

é o resultado ótimo, todas as threads vão para a outra etapa.

Na verify_end_condition (Algoritmo 8, linha 8), as threads não expandem mais ne- nhum nó, uma variável booleana em memória compartilhada bs recebe o valor verdadeiro

e todas as threads são sincronizadas. Depois, todas as threads consomem suas filas e cada thread ti verifica se f(Nf) < OpenListi.lowest_f () é verdadeiro. Ou seja, verifica se a

possível resposta possui melhor prioridade do que todos os nós da OpenListi. Senão, bs

recebe falso. Todas as threads são sincronizadas novamente. Depois, se bs é verdadeiro,

então o nó Nf é o resultado ótimo e verify_end_condition retorna verdadeiro e o algo-

ritmo retorna Nf como resposta (Algoritmo 8, linha 10). Caso contrário existe um ou

mais nós em alguma OpenList possui um melhor valor f e precisam ser expandidos antes que seja possível garantir que Nf é o resultado ótimo. Para reiniciar a etapa de busca,

o nó Nf é inserido na OpenList da thread que o expandiu pela primeira vez, a função

verify_step retorna falso e todas as threads voltam à etapa de busca search_step.

Além disso, algumas outras condições especiais devem ser verificadas. Primeiro, quando a thread ti chama consume_queue, mas a OpenListi está vazia, ela precisa

process_f inal_node. Se uma thread expande um nó final Nf 2, e um nó Nf 1 já foi ex-

pandido, é necessário verificar se f(Nf 2) < f (Nf 1). Se isso for verdadeiro, significa que

uma resposta melhor foi encontrada enquanto as threads consumiam as suas queues e ex- pandiam as OpenLists. Neste caso, a verificação de Nf 1 como possível resposta deve ser

cancelada, e o processo deve ser reiniciado para verificar se Nf 2 é uma possível resposta.

Para isso, o contador de threads threads_counter é reiniciado para 1 e o nó Nf 2 é inserido

em todas as queues. Após isso, todas as threads iniciam a etapa de busca search_step. Antes de iniciar o PA-Star, algumas vezes é necessário efetuar operações para a função heurística h. No caso do alinhamento múltiplo de sequências, utilizamos a função h2,all

(Seção 2.3.2.2), que exige a comparação ótima de todos as sequências par-a-par. Com a divisão de verificação de parada em duas etapas, overhead de sincronização entre as threads é reduzido, interrompendo a etapa de busca e sincronizando as threads apenas se uma possível resposta for encontrada.