Concorrência e Paralelismo mleal@inf.puc-rio.br 1 Programação Concorrente e Paralela Na programação sequencial todas as instruções de um programa são executadas através de uma única linha de execução Na programação concorrente e paralela um programa pode ser executado através de diversas linhas de execução. mleal@inf.puc-rio.br 2
Programação Concorrente e Paralela fluxo único de execução vários fluxos de execução tarefa 1 tarefa 2 tarefa 1 tarefa 2 tarefa 3 tarefa 3 mleal@inf.puc-rio.br 3 Programação Concorrente e Paralela Os mecanismos para programação concorrente e paralela compreendem as construções que LPs usam para: indicar quais unidades são concorrentes; ativar e controlar o fluxo de execução de unidades concorrentes; possibilitar a interação e sincronização entre unidades concorrentes. mleal@inf.puc-rio.br 4
Porque Usar Programação Concorrente? Ganhos de desempenho em sistemas com múltiplos processadores (paralelismo físico). Ganhos de desempenho através de acesso concorrente a dispositivos de hardware - o acesso a discos ou a dispositvos de rede não precisa bloquear toda a aplicação, mas apenas um thread. Maior responsividade da aplicação - threads com alta prioridade podem ser associadados a tarefas críticas (ex: vídeo). Maior facilidade em capturar e estruturar a lógica de certas aplicações (aplicações iterativas, servidores,, etc). mleal@inf.puc-rio.br 5 Paralelismo Físico e Lógico A 3 processos 3 processadores B C paralelismo físico tempo 3 processos 1 processador A B C paralelismo lógico tempo mleal@inf.puc-rio.br 6
Processos Um processo é um fluxo de controle sequencial e seu espaço de endereçamento (registradores, memória e arquivos). Informalmente um processo é a execução de um programa junto com os dados usados por ele. A criação, execução e o encerramento de processos são tratados diretamente pelo sistema operacional. mleal@inf.puc-rio.br 7 Estados de um Processos 1 executando 3 2 1. Processo é bloqueado por operação de I/O 2. Escalonador escolhe um novo processo bloqueado pronto 3. Novo processo começa a executar 4 4. Operação de I/O é completada mleal@inf.puc-rio.br 8
Threads Um processo tem duas partes: O fluxo de controle; O espaço de endereçamento (memória e outros recursos como I\O). I Um thread consiste somente no fluxo de controle e numa pilha de memória independente. O escalonamento de um thread é uma operação mais simples, já que não envolve a mudança integral do espaço de endereçamento. mleal@inf.puc-rio.br 9 Criação de Processos A criação de novos processos sempre é controlada pelo SO Um processo é uma abstração do SO. Em Unix processos podem ser criados explicitamente através do comando fork(): pid = fork(); if ( pid < 0 ) { printf(``cannot fork!!n''); exit(1); if ( pid == 0 ) { /* Child process */... else { /* Parent process pid is child's pid */... mleal@inf.puc-rio.br 10
Criação de Threads O suporte a threads pode acontecer através da semântica básica da LP ou através de bibliotecas e pacotes específicos. Alguns SOs oferecem suporte a threads (Windows XP, Linux) mas nem todos (diversas implementações de Unix). Exemplos de LPs com suporte explícito a concorrência são Algol, Ada, Modula 3, Occam, Java e C#. Exemplos de pacotes e bibliotecas para programação concorrente e paralela são PVM e MPI (Fortran, C/C++) e pthreads (C/C++). mleal@inf.puc-rio.br 11 Criação de Threads Os principais mecanismos de criação de threads são: Co-begin; Loops paralelos; Instanciação na elaboração; Instanciação explícita. mleal@inf.puc-rio.br 12
Co-begin Blocos de código que podem ser executados de forma concorrente são delimitados por uma declaração especial (par( par). Em Algol temos: par begin # comandos neste bloco são concorrentes # p(a) begin # comandos neste bloco são sequenciais # b: = x + y; c:= f (b, d) end p(f) end mleal@inf.puc-rio.br 13 Loops Paralelos Em algumas LPs as iterações de um loop podem ser executadas de forma concorrente através de uma construção específica. Em Occam temos: co (i:= 1 to 10) -> f(a, b, i) oc # são criados 10 threads# mleal@inf.puc-rio.br 14
Instanciação na Elaboração Em Ada e SR o código de um thread pode ser definido de forma semelhante a uma subrotina. Quando a subrotina é invocada, o thread é criado automaticamente e começa a executar. procedure P is task T is... end T; begin - corpo de P... end P; mleal@inf.puc-rio.br 15 Instanciação Explícita Normalmente bibliotecas e pacotes com suporte a threads permitem apenas a criação explícita de threads. Em pthreads um novo thread é criado explicitamente através do comando pthread_create() que recebe como parâmetro a rotina a ser executada pelo novo thread. Em Java um thread é criado através da instanciação de um objeto de uma classe que representa um novo thread. mleal@inf.puc-rio.br 16
Instanciação Explícita Exemplo C pthreads: int main(int argc, char *argv[]) {... for (i=0; i< n_workers; i++){ erro = pthread_create(&tid[i], NULL, worker,(void *) i); if (erro){ printf("erro na criacao do thread!!"); exit(1); void *worker(void *arg) { int id = (int) arg; printf("sou o worker %d\n",id);... mleal@inf.puc-rio.br 17 Sincronizacão Aplicações multi-threads threads precisam usar mecanismos específicos para controlar a iteração entre os diferentes threads. A sincronização entre threads pode ser feita através de dois mecanismos básicos: memória compartilhada e troca de mensagens. Os dois principais mecanismos de sincronização com memória compartilhada são semáforos e monitores. mleal@inf.puc-rio.br 18
Semáforos Semáforos são estruturas de dados especiais que podem assumir apenas valores inteiros, sendo manipulados através das funções P ( ou down) ) e V ( ou up). Para um semáforo s temos: P(s) se se s>0 então s= s -1 senão supender a execução do thread corrente V(s) se se existe algum thread suspenso então escolher um qualquer e executá-lo senão s=s+1 mleal@inf.puc-rio.br 19 Mutex Mutex são semáforos binários que podem assumir apenas os valores 0 e 1. São utilizados para permitir o acesso exclusivo a recursos ou para definir regiões críticas do programa (trechos de código que não podem ser executados concomitantemente): sem mutex = 1 process CS [i=1 to n]{ while (true){ P(mutex); região crítica; V(mutex); região não crítica mleal@inf.puc-rio.br 20
Problema do Produtor\Consumidor produtores put buffer get consumidores capacidade = n O buffer possui tamanho limitado (n). Só podem ser inseridos produtos se o buffer não estiver cheio. Só podem ser retirados produtos se o buffer não estiver vazio. mleal@inf.puc-rio.br 21 Problema do Produtor\Consumidor Solução utiliza três semáforos: Full: : conta o número de slots no buffer que estão ocupados; Empty: : conta o número de slots no buffer que estão vazios; Mutex: : garante que os processos produtor e consumidor não acessam o buffer ao mesmo tempo; mleal@inf.puc-rio.br 22
# include prototypes.h # define N 100 typedef int semaphore; semaphore mutex = 1; semaphore empty = N; semaphore full = 0; void producer (void){ int item; while (TRUE){ produce_item(&item); P(&empty); P(&mutex); enter_item(item); V(&mutex); V(&full); void consumer (void){ int item; while (TRUE){ P(&full); P(&mutex); remove_item(item); V(&mutex); V(&empty); consume_item(item); mleal@inf.puc-rio.br 23 Deadlock O uso inadequado de mecanismos de sincronização pode levar a situações em que todos os threads de uma aplicação ficam bloqueados a espera de uma condição que nunca ocorre. O deadlock é um dos erros mais comuns em aplicações multi-threads. threads. mleal@inf.puc-rio.br 24
Jantar dos Filósofos process filosos [1=0 to 4]{ while (true){ pensar; pegar_garfos; comer; soltar_garfos; deadlock mleal@inf.puc-rio.br 25 Jantar dos Filósofos Solução usando semáforos sem garfo[5] = {1, 1, 1, 1, 1 process filosofo [1=0 to 3]{ while (true){ P(garfo[i]);P(garfo[i+1]); comer; V(garfo[i]);V(garfo[i+1]); pensar; process filosofo [4]{ while (true){ P(garfo[0]);P(garfo[4]); comer; V(garfo[0]);V(garfo[4]); pensar; mleal@inf.puc-rio.br 26
Monitores Monitores são construções utilizadas para a sincronização de threads que encapsulam um conjunto de procedimentos e variáveis. Somente um thread pode estar ativo dentro do monitor em qualquer instante. Outros threads interessados em executar rotinas de um monitor ficam bloqueados até que o thread ativo termine de executar o código do monitor. mleal@inf.puc-rio.br 27 Monitores monitor example int i; condition c; procedure producer();. end; procedure consumer();. end; end monitor; mleal@inf.puc-rio.br 28
Monitores Monitores utilizam variáveis condicionais que permitem que o thread em execução seja suspenso se uma determinada condição não for observada. O bloqueio do thread ocorre através de uma chamada do tipo wait(variável condicional). Quando um thread é bloqueado em uma variável condicional, um novo thread pode entrar no monitor. Threads bloqueados por variáveis condicionais podem ser acordados através de uma chamada do tipo signal(variável condicional) executada por outro thread. O thread bloqueado só é ativado quando o thread ativo no monitor sai do monitor. mleal@inf.puc-rio.br 29 Produtor\consumidor usando monitores monitor Bounded_Buffer { typet buf[n]; int front = 0, rear = 0, count = 0; cond not_full, not_empty; procedure deposit (typet data){ while (count == n) wait (not_full); buf[rear] = data; rear = (rear+1) % n; count++; signal(not_empty); end; procedure fetch(typet &result){ while (count == 0) wait (not_empty); result = buf[front]; fornt = (fornt+1) % n; count-- --; signal(not_full); mleal@inf.puc-rio.br 30
Troca de Mensagens Uma segunda alternativa pra a sincronização entre processos e threads é a troca de mensagens. É a única alternativa de sincronização em sistemas distribuídos. A troca de mensagens é geralmente implementada através de duas primitivas: send (destination,message) - envia para um determinado destino uma mensagem. receive (source,message) - recebe a mensagem de uma determinada fonte. mleal@inf.puc-rio.br 31 Troca de Mensagens As chamadas send e receive podem ser síncronas ou assíncronas. Em um send síncrono o thread que fez a invocação pode ficar bloqueado até a mensagem ser recebida pela dispositivo de rede, ou mesmo até o recebimento de uma resposta. No caso do receive síncrono o thread que fez a invocação pode ficar bloqueado até que a mensagem desejada seja recebida. Nas chamadas assíncronas o thread que fez a invocação nunca fica bloqueado. mleal@inf.puc-rio.br 32
RPC A chamada de procedimento remoto (remote procedure call) é um modelo de nível mais alto para a troca de mensagens entre processos. Permite a invocação de subrotinas entre processos que estão sendo executados em diferentes computadores de uma rede. A semântica de uma chamada remota é semelhante a de uma chamada local. mleal@inf.puc-rio.br 33 RPC cliente servidor processo cliente 1 10 processo servidor 6 5 stub do cliente stub do servidor 2 9 interface de rede 8 3 7 4 interface de rede mleal@inf.puc-rio.br 34
RPC 1. O processo cliente chama (chamada local) o stub do cliente, com os parâmetros apropriados; 2. O stub organiza os parâmetros e monta uma mensagem, endereçada ao servidor, que é passada a interface de rede; 3. A mensagem é enviada da máquina cliente para a máquina servidor; 4. A interface de rede do servidor passa a mensagem ao stub do servidor; 5. O stub do servidor desmonta a mensagem, recupera os parâmetros, e chama (chamada local) o programa servidor, passando os parâmetros; 6. Após sua execução, o programa envia os eventuais resultados para o stub do servidor; 7. O stub do servidor monta uma mensagem, endereçada ao stub do cliente, com a indicação do fim da execução e com os resultados, e passa a mensagem à interface de rede; 8. A mensagem é enviada da máquina servidor para a máquina cliente; 9. A interface de rede do cliente passa a mensagem ao stub do cliente; 10. O stub do cliente desmonta a mensagem, recuperando a indicação do fim da execução e os resultados, e retorna da chamada local, passando o controle e os resultados ao cliente. mleal@inf.puc-rio.br 35 RMI A invocação remota de métodos (remote method invocation) é semelhante à RPC, oferecendo um modelo de alto nível para a invocação de métodos de objetos remoto. É necessário que o programa cliente possua uma referência remota para o objeto cujo método deseja invocar. Isso é possível através de um servidor de nomes. O cliente envia ao servidor de nomes o nome de um objeto, e recebe como resposta uma referência remota para o mesmo. mleal@inf.puc-rio.br 36
Problemas na Utilização de Threads A necessidade de utilização de mecanismos de sincronização para o acesso a recursos compartilhados. A possibilidade de deadlock como resultado da utilização de mecanismos de sincronização. Uma maior dificuldade no processo de debug de um programa em decorrência da forma quase que aleatória com que os threads são ativados pelo escalonador. Uma degradação do desempenho do sistema em função do aumento do número de threads ou da granularidade com que são empregados os mecanismos de sincronização. mleal@inf.puc-rio.br 37 Programação Orientado a Eventos Muitas aplicações são naturalmente estruturadas a partir da ocorrência de eventos externos. Sistemas interativos com interfaces gráficas, como um processador de textos, executam funções quase que exclusivamente como resposta a ações de usuários. A cada evento é associada uma reação, representada por uma rotina ou handler,, e o fluxo de processamento consiste num encadeamento de reações. mleal@inf.puc-rio.br 38
Programação Orientado a Eventos Aplicações baseadas em eventos geralmente são compostas por um loop contínuo que aguarda pela ocorrência de eventos. Assim que um evento é registrado um coletor de eventos insere um objeto associado ao evento em uma fila. Um processador de eventos retira os objetos da fila e executa as reações (rotinas) correspondentes. mleal@inf.puc-rio.br 39 Programação Orientado a Eventos componente 1 componente 2 coletor de eventos fila de eventos eventos processador de eventos componente 3 mleal@inf.puc-rio.br 40
Programação Orientado a Eventos Os sistemas orientados a eventos podem ser divididos em dois modelos. No modelo preemptivo cada tipo de evento possui uma prioridade, e a execução de uma reação associada a um evento é suspensa sempre que um evento de maior prioridade for recebido. No modelo não preemptivo as reações são executadas de forma atômica, e novos eventos só são tratados após ser completado o tratamento de eventos anteriores. mleal@inf.puc-rio.br 41 Programação Orientado a Eventos No modelo preemptivo não existe portanto a necessidade de utilização de mecanismos como semáforos ou monitores para o controle de acesso a recursos compartilhados. A invocação das funções potencialmente bloqueantes segue sempre uma disciplina assíncrona, e os resultados são tratados através de handlers passados como parâmetros das chamadas. O maior problema da programação orientada a eventos é a necessidade de quebrar o programa em blocos separados, o que pode dificultar a compreensão da linha de execução. mleal@inf.puc-rio.br 42
Programação Orientado a Eventos void main (){ do{ receive(msg); print(msg); while(msg) usando orientação a eventos void function print_msg (string msg){ if (msg){ print(msg); receive (print_msg); void main() { receive (print_msg); mleal@inf.puc-rio.br 43 Continuações Uma continuação pode ser entendida como o que falta a ser computado em um programa. Considere o programa: x = 10; y =15; x = x+y; print (x); Após a instrução x=10 a continuação é y=15; x = x+y; print (x); Após a instrução y = 15 a continuação é y=15; x = x+y; print (x); mleal@inf.puc-rio.br 44
Continuações Algumas LPs, como C e Scheme, permitem a captura e representação explícita de uma continuação. Em Scheme uma continuação pode ser criada com a função call/cc. Em C continuações podem ser usadas através das funções setjmp e longjmp. Ao invocar uma continuação o programa abandona a execução corrente e retoma a execução representada pela continuação. mleal@inf.puc-rio.br 45 Exemplo #include <setjmp.h> jmp_buf env; void int teste(){ int i; i = setjmp(env); // retorna 0 quando a continuação // é criada, ou o argumento de longjmp if (i=0){... /* Faz alguma coisa*/ if (erro) longjmp(env, erro); // chama a continuação... else { printf( Erro: %d/n,i) mleal@inf.puc-rio.br 46
Corotinas Corotinas são construções que permitem que rotinas de um programa possam ser executadas de forma alternada, em intervalos intercalados no tempo. Enquanto threads são escalonados de forma preemptiva, corotinas são escalonadas manualmente pelo programa através de chamadas específicas (resume,( yield, transfer, etc). Não existe portanto a necessidade de utilização de mecanismos como semáforos ou monitores para o controle de acesso a recursos compartilhados. Poucas LPs suportam corotinas. Exemplos são Simula, Modula 2 e Lua a partir da versão 5.0. mleal@inf.puc-rio.br 47 program exemplo; var i:item; coroutine produtor; begin loop produzir(i); resume consumidor; end; end; coroutine consumidor; begin loop consumir(i); resume produtor; end; end; begin resume produtor; end. mleal@inf.puc-rio.br 48
Corotinas - Implementação No modelo tradicional de invocação de subrotinas uma subrotina sempre está subordinada a outra. Neste caso pode-se usar uma única pilha para representar a execução de cada subrotina. Corotinas executam de forma cooperativa e não subordinada. Não existe portanto uma hierarquia natural entre elas. Para cada corotina é necessário uma pilha independente, já que sua linha de execução pode incluir a invocação de subrotinas. mleal@inf.puc-rio.br 49 Corotinas Árvore de pilhas ou stack cactus mleal@inf.puc-rio.br 50