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.