4ª Lista de Exercícios de Paradigmas de Linguagens Computacionais Professor: Fernando Castor
Monitores: Agay Borges (abn), Cleivson Siqueira (csa3),
Dennis Silveira (dwas), Eduardo Rocha (ejfrf),
Lívia Vilaça (lcjbv), Lucas Inojosa (licf), Luís Gabriel Lima (lgnfl),
Walter Ferreira (wflf), Wellington Oliveira (woj)
CIn-‐UFPE – 2011.1 Disponível desde: 09/06/2011
Entrega: 28/06/2011
A lista deverá ser respondida em dupla. A falha em entregar a lista até a data estipulada implicará na perda de 0,25 ponto na média da disciplina para os membros da dupla. Considera-‐se que uma lista na qual menos que 5 questões foram respondidas corretamente não foi entregue. A entrega da lista com pelo menos 7 questões corretamente respondidas implica em um acréscimo de 0,125 ponto na média da disciplina para os membros da dupla. Se qualquer situação de cópia de respostas for identificada, os membros de todas as duplas envolvidas perderão 0,5 ponto na média da disciplina. O mesmo vale para respostas obtidas a partir da Internet. As respostas deverão ser entregues exclusivamente em formato texto ASCII (nada de .pdf, .doc, .docx ou .odt) e deverão ser enviadas para o monitor responsável por sua dupla, sem cópia para o professor. Tanto podem ser organizadas em arquivos separados, um por questão (e, neste caso, deverão ser zipadas), quanto em um único arquivo texto onde a resposta de cada questão está devidamente identificada e é auto-‐contida (uma parte da resposta de uma questão que seja útil para responder uma outra deve estar duplicada neste última). As questões 9 e 10 são obrigatórias.
1) Desenvolva novamente a solução da multiplicação paralela da última lista sem a utilização do operador de multiplicação, agora em Haskell. A solução utilizará paralelismo semi-‐explícito. Defina uma constante 'n' que definirá o número de tarefas paralelas a serem executadas pela multiplicação, no mesmo estilo da questão da lista anterior, e uma função multiplicar :: Integer -> Integer -> Integer.
Provavelmente a sua questão utilizará solução recursiva pura, então não use valores tão altos para o fator de multiplicação como a questão da lista anterior, com risco de demorar demais e/ou estourar a pilha.
2) Crie um contador em Haskell utilizando variáveis compartilhadas (MVars). No main, você deverá criar um número 2n de threads, n incrementando e n decrementando. Cada thread deverá realizar sua tarefa (++ ou -‐-‐) 1000000/n vezes. Você terá de analisar, para alguns n's, o tempo de execução e custo do programa e explicar o que acontece quando n tende a um número grande, e o porquê destes fatos.
Por exemplo, para n = 1:
thread 1: incrementador; até 1000000; thread 2: decrementador; até 1000000.
thread 1: incrementador; até 500000; thread 2: decrementador; até 500000; thread 3: incrementador; até 500000; thread 4: decrementador; até 500000.
3) Considere a implementação abaixo da função criar_arvore_completa, que recebe um inteiro n e cria uma árvore binária completa com n nós.
data Tree t = EmptyTree | Node t (Tree t) (Tree t) deriving (Show)
nivel :: Float -‐> Int -‐> Int nivel 0 _ = 0
nivel n x = if n >= 2 then nivel (n / 2) (x + 1) else x
criar_arvore :: Int -‐> Int -‐> Int -‐> Tree Int criar_arvore n _ 0 = EmptyTree
criar_arvore n x y
| x > n = EmptyTree
| otherwise = Node x (criar_arvore n (x*2) (y -‐ 1)) (criar_arvore n ((x*2) + 1) (y -‐ 1))
criar_arvore_completa :: Int -‐> Tree Int criar_arvore_completa 0 = EmptyTree
criar_arvore_completa n = Node 1 (criar_arvore n 2 (nivel (fromIntegral n) 0)) (criar_arvore n 3 (nivel (fromIntegral n) 0))
Exemplo:
*Main> criar_arvore_completa 10
Node 1 (Node 2 (Node 4 (Node 8 EmptyTree EmptyTree) (Node 9 EmptyTree EmptyTree)) (Node 5 (Node 10 EmptyTree EmptyTree) EmptyTree)) (Node 3 (Node 6 EmptyTree EmptyTree) (Node 7 EmptyTree EmptyTree))
Considere também a implementação abaixo que calcula um balance especial para uma dada raiz. O balance é calculado da seguinte forma: calcula-‐se o valor do peso dos filhos da esquerda, calcula-‐se o valor do peso dos filhos da direita, e em seguida realiza-‐se a subtração do peso do lado esquerdo com o peso do lado direito. O peso é calculado somando o valor módulo 11311 de todos os nós da árvore.
balanceRaiz :: Tree Int -‐> Int balanceRaiz EmptyTree = 0
balanceRaiz (Node t a b) = (valorPeso a) -‐ (valorPeso b)
valorPeso :: Tree Int -‐> Int valorPeso EmptyTree = 0
valorPeso (Node t a b) = mod ((mod t 11311) + (valorPeso a) + (valorPeso b)) 11311
Desenvolva, usando paralelismo semi-‐explícito, uma versão paralela e mais eficiente da implementação acima. Deve-‐se utilizar como parâmetro para a função balanceRaiz árvores geradas pela função criar_arvore_completa para valores muito grandes (N = 10000).
import System.Random
data Tree a = NilT | Node a (Tree a) (Tree a) deriving (Eq, Show)
randomList :: Int -‐> [Int]
randomList l = randoms (-‐l, l) (mkStdGen l)
criaTree :: Int -‐> Tree Int criaTree 0 = NilT
criaTree h = foldr (fillTree) NilT (take (2^h-‐1) (randomList 50))
fillTree :: Int -‐> Tree Int -‐> Tree Int fillTree n NilT = Node n NilT NilT fillTree n (Node x left right)
| (left == NilT) && (right == NilT) = (Node x (fillTree n left) right) | (left == NilT) && (right /= NilT) = (Node x (fillTree n left) right) | (left /= NilT) && (right == NilT) = (Node x left (fillTree n right)) | (left /= NilT) && (right /= NilT) && ((qtdFilhos left) >= ((qtdFilhos right))) = (Node x left (fillTree n right))
| otherwise = (Node x (fillTree n left) right)
qtdFilhos :: Tree Int -‐> Int qtdFilhos NilT = 0
qtdFilhos (Node x left right)
| (left == NilT) && (right == NilT) = 0
| (left /= NilT) && (right == NilT) = qtdFilhos left + 1 | (left == NilT) && (right /= NilT) = qtdFilhos right + 1 | otherwise = qtdFilhos right + qtdFilhos left + 2
somaTree :: Tree Int -‐> Int somaTree (NilT) = 0
somaTree (Node x left right) = x + (somaTree left) + (somaTree right)
somatorio :: Int -‐> Int
somatorio h = somaTree (criaTree h)
Crie outras soluções para este problema utilizando as seguintes abordagens:
A) Uma solução com um número fixo de threads (à sua escolha, aconselha-‐se duas ou quatro); B) Uma solução sem um número fixo de threads;
Qual a altura máxima, nas condições de sua máquina, que esta árvore pode atingir para que as soluções A e B sejam mais eficientes que a solução sequencial? Explique.
Qual a altura máxima, nas condições de sua máquina, que esta árvore pode atingir para que a solução B seja mais eficiente que a solução A? Explique.
5) Considere a seguinte implementação:
collatzFunction :: Integer -‐> Integer collatzFunction n
| (mod n 2) == 1 = (3*n)+1 | otherwise = div n 2
collatzSequence :: Integer -‐> [Integer] collatzSequence n
Desenvolva uma solução utilizando variáveis compartilhadas em Haskell para a implementação sequencial acima. O usuário deverá estabelecer o valor n a ser calculado e o número de threads a serem utilizadas, através do console. Faça um comparação dos resultados apresentados por essa solução considerando o tempo de execução para diferentes quantidades de threads (cujos valores não sejam muito próximos entre si) e para implementação sequencial. Determine em qual situação a solução foi processada da maneira mais eficiente e explique o porquê. Utilize valores muito grandes para n (preferencialmente com cerca de 10 casas decimais ou mais).
6) Implemente uma solução em Haskell para uma variação do problema do Produtor e do Consumidor onde há vários (dois ou mais, conforme definido pelo usuário) produtores e cada um tem seu próprio buffer (um por produtor). Os buffers de todos os produtores têm a mesma capacidade (pelo menos 10 posições). Além disso, o sistema tem vários consumidores (mesma quantidade que a de produtores). Cada produtor coloca itens produzidos apenas em seu buffer particular mas consumidores podem consumir itens de qualquer buffer. Fora isso, as mesmas regras do Produtor-‐Consumidor básico valem: consumidores não podem consumir de um buffer vazio, embora possam tentar consumir de outro buffer, e produtores não podem produzir além da capacidade de seus buffers. Além disso, o sistema nunca pode entrar em deadlock. Implemente a solução utilizando memória transacional e avalie o desempenho da sua solução considerando que os produtores produzem, cada um, pelo menos 1.000.000 de itens (e não esperam para cada item produzido, antes de produzir o próximo).
7) Considere o seguinte jogo: No início é determinado o tempo de duração do mesmo, vários jogadores tem um número identificador e o objetivo deles é ter o maior número de posições do tabuleiro com o seu número. Ao final do tempo pré-‐determinado, o jogo deve terminar bem como a execução do programa. As regras são as seguintes:
• O tabuleiro do jogo é uma lista de tamanho n;
• O tabuleiro é inicialmente preenchido de forma que todos os jogadores tenham uma quantidade igual de posições, caso isso seja impossível, a distribuição de posições deve ser a mais homogênea possível;
• Um jogador só poderá modificar as posições que contém o número identificador de um dos jogadores adjacentes a ele; (no caso do primeiro jogador, os adjacentes a ele são o último e o segundo, no caso do último jogador, o penúltimo e o primeiro)
• Enquanto um jogador estiver preenchendo uma determinada posição da lista com o seu número outro jogador não pode fazer o mesmo;
• Se ao tentar preencher uma determinada posição ela já estiver em uso, o jogador deve procurar a próxima posição válida;
• Cada um dos jogadores deve operar numa Thread independente;
• O número de jogadores, o tamanho do tabuleiro e o tempo de execução devem ser determinados pelo usuário;
• No final da execução deve ser exibido na tela o estado final do tabuleiro bem como a identificação de qual dos jogadores conseguiu marcar mais posições, se houver um empate então deve ser exibida uma lista com todos os jogadores que empataram.
• Não deve haver deadlock.
Utilize threads explicitas em Haskell e variáveis compartilhadas para a resolução do problema.
Considere a seguinte lista como um exemplo de tabuleiro já com sua distribuição inicial. 1 4 2 3 5 3 2 1 5 4
Quando o jogador 1 tentar executar uma ação, ele só poderá modificar os números do jogador 5 e 2. Alguns exemplos de como poderiam ser as suas jogadas:
Caso em que o jogador 1 pega todas as casas dos jogadores 5 e 2. 1 4 1 3 1 3 1 1 1 4
Caso em que o jogador 3 pegou a 3ª casa e o jogador 1 só teve tempo para pegar a 5ª casa. 1 4 3 3 1 3 2 1 5 4
Caso em que o jogador 1 não conseguiu pegar casa alguma uma vez que todas estavam sendo usadas no momento em que ele tentou.
1 4 3 3 4 3 3 1 4 4
Caso em que o jogador 1 pegou 9ª casa e a thread dele foi trocada. 1 4 2 3 5 3 2 1 1 4
8) Carlos tem uma namorada, Roberta, e está pensando em começar um relacionamento triplo adicionando Laura ao namoro. Ele sabe que namorar é uma coisa complicada, ainda mais com duas mulheres, mas não tem noção se pode dar certo ou não. Para ajudá-‐lo (ou prejudicá-‐lo, depende do ponto de vista) faça um programa que simule o relacionamento, seguindo as seguintes regras:
• Se em um mês as duas namoradas se veem mais do que veem Carlos, as duas brigam entre si;
• Se Carlos passar dois meses vendo a mesma namorada, a outra briga com ele;
• Se Carlos passar três meses vendo a mesma namorada ela perdoa uma briga (a outra continua brigando com ele -‐-‐ conta como uma briga a mais);
• Se os 3 conseguirem ultrapassar os 10 anos de namoro triplo, o namoro não acaba mais; • Se alguma das namoradas atingir 60 brigas ela acaba o namoro;
• Se Carlos atingir 80 brigas ele acaba o namoro com as duas;
• O programa deve imprimir o numero de brigas de cada um e o tempo passado, em meses, ao final da sua execução;
• O programa deve ser em Haskell usando apenas memoria transacional.
9) Considere que uma galinha pode ser modelada por 4 Threads. Cada thread representa a função de uma parte do corpo.
Os olhos procuram por comida e informam ao cérebro sempre que alguma comida for
detectada. Uma vez que a comida é localizada, o cérebro avisa aos pés para se moverem uma certa distância com o objetivo de alcançar a comida. Uma vez que os pés tiverem movido a distância indicada, eles informam ao cérebro, e esse, por sua vez, avisa a boca para começar a comer.
Essas galinhas não tem memória, a qualquer momento os olhos podem identificar outra comida. Isso vai acarretar em: o cérebro avisar aos pés para parar qualquer caminhada e a boca parar de comer, os pés começarem a se mover em direção a nova comida, e em seguida
Escreva um programa que simula o comportamento dessa galinha. As threads devem se comportar como descrito abaixo:
-‐ Olhos: procuram por comida a cada tempo t ms (t está entre 1000 e 2000), achando comida 50% das vezes. Cada vez que os olhos buscam por comida a string "procurando..." é imprimida.
-‐ Cérebro: recebe sinal dos olhos indicando que foi encontrado comida. Uma vez informado, a string "comida!" é impressa e em seguida informa a boca para parar de comer, aos pés para pararem qualquer movimentação e se moverem em direção à nova comida encontrada. Uma vez que
os pés indicam que o destino foi alcançado a string "yes!" é impressa e a boca é avisada para
começar a comilança.
-‐ Pés: recebem sinal do cérebro indicando nova comida. Uma vez avisados, param qualquer
movimento que esteja sendo feito, e movem-‐se um número randômico de passos (entre 5 e 10) en direção à nova comida. São gastos 400ms por passo. Depois de cada passo deve-‐se imprimir a string "passo". Uma vez que uma sequência de passos tenha sido finalizada, o cérebro deve ser avisado que o destino já foi alcaçado.
-‐ Boca: aguarda por um sinal do cérebro para começar a comer. Fica comendo até que seja informada para parar. Cada bocanhada leva 400ms e para cada uma deve ser impresso uma string "comendo".
A simulação só deve terminar quando o usuário matar o processo. Sua implementação
deve usar apenas memória transacional e variáveis transacionais. É
probido
usar MVars e a
função par.