You are on page 1of 22

PROGRAMAÇÃO CONCORRENTE

A   programação   concorrente   foi   usada   inicialmente   na   construção   de   sistemas   operacionais.
Atualmente, ela é usada para desenvolver aplicações em todas as áreas da computação. Este tipo
de programação tornou­se ainda mais importante com o advento dos sistemas distribuídos e das
máquinas com arquitetura paralela. Neste capítulo serão apresentados os conceitos básicos e os
mecanismos clássicos da programação concorrente. Maiores detalhes podem ser encontrados no
livro [TOS03].
1 Definição
A   grande   maioria   dos   programas   escritos   são  programas   seqüenciais.   Nesse   caso,   existe
somente um fluxo de controle (fluxo de execução, linha de execução, thread) no programa. Isso
permite, por exemplo, que o programador realize uma "execução imaginária" de seu programa
apontando com o dedo, a cada instante, o comando que está sendo executada no momento.
Um  programa concorrente  pode ser visto como se tivesse vários fluxos de execução.
Para o programador realizar agora uma "execução imaginária", ele vai necessitar de vários dedos,
um para cada fluxo de controle. 
O   termo   "programação   concorrente"   vem   do   inglês  concurrent   programming,   onde
concurrent  significa   "acontecendo   ao   mesmo   tempo".   Uma   tradução   mais   adequada   seria
programação  concomitante.  Entretanto,  o termo  programação  concorrente  já está solidamente
estabelecido no Brasil. Algumas vezes é usado o termo  programação paralela  com o mesmo
sentido.
É   comum   em   sistemas   multiusuário   que   um   mesmo   programa   seja   executado
simultaneamente   por   vários   usuários.   Por   exemplo,   um   editor   de   texto.   Entretanto,   ter   10
execuções simultâneas do editor de texto não faz dele um programa concorrente. O que se tem
são 10 processos independentes executando o mesmo programa seqüencial (compartilhando o
mesmo   código).   Cada   processo   tem   a   sua   área   de   dados   e   ignora   a   existência   das   outras
execuções do programa. Esses processos não interagem entre si (não trocam informações). Um
programa é considerado concorrente quando ele (o próprio programa, durante a sua execução)
origina diferentes processos. Esses processos, em geral, irão interagir entre si.
2 Motivação
A   programação   concorrente   é   mais   complexa   que   a   programação   seqüencial.   Um   programa
concorrente pode apresentar todos os tipos de erros que aparecem nos programas seqüenciais e,
adicionalmente, os erros associados com as interações entre os processos. Muitos erros dependem
do exato instante de tempo em que o escalonador do sistema operacional realiza um chaveamento
de contexto. Isso torna muitos erros difíceis de reproduzir e de identificar.
Apesar   da   maior   complexidade,   existem   muitas   áreas   nas   quais   a   programação
concorrente é vantajosa. Em sistemas nos quais existem vários processadores (máquinas paralelas
ou   sistemas   distribuídos),   é   possível   aproveitar   esse   paralelismo   e   acelerar   a   execução   do

PROGRAMAÇÃO CONCORRENTE – Prof. Simão Toscani
(Do livro Sistemas Operacionais e Programação Concorrente, Toscani S.S., Oliveira R.S. e Carissimi A.S.
Editora Sagra-Luzzatto, 2003)

1

programa. Mesmo em sistemas com um único processador, existem razões para o seu uso em
vários tipos de aplicações.
Considere um programa que deve ler registros de um arquivo, colocar em um formato
apropriado e então enviar para uma impressora física (em oposição a uma impressora lógica ou
virtual,  implementada  com arquivos).  Podemos  fazer isso com  um programa  seqüencial  que,
dentro de um laço, faz as três operações (ler, formatar e imprimir registro). 

Arquivo

Processo

Impressora
física

Figura 1 ­ Programa seqüencial acessando arquivo e impressora.
Inicialmente o processo envia um comando para a leitura do arquivo e fica bloqueado. O
disco   então   é   acionado   para   realizar   a   operação   de   leitura.   Uma   vez   concluída   a   leitura,   o
processo realiza a formatação e inicia a transferência dos dados para a impressora. Como trata­se
de uma impressora física, o processo executa um laço no qual os dados são enviados para a porta
serial ou paralela apropriada. Como o buffer da impressora é relativamente pequeno, o processo
fica preso até o final da impressão. O disco e a impressora nunca trabalham simultaneamente,
embora isso seja possível. É o programa seqüencial que não consegue ocupar ambos.
Vamos agora considerar um programa concorrente como o mostrado na figura 2 para
realizar   a   impressão   do   arquivo.   Dois   processos   dividem   o   trabalho.   O   processo   leitor   é
responsável   por   ler   registros   do   arquivo,   formatar   e   colocar   em   um  buffer  na   memória.   O
processo impressor retira os dados do  buffer  e envia para a impressora. É suposto aqui que os
dois processos possuem acesso à memória onde está o  buffer.  Este programa é mais eficiente,
pois consegue manter o disco e a impressora trabalhando simultaneamente. O tempo total para
realizar a impressão do arquivo vai ser menor.

Arquivo

Processo
Leitor

Buffer

Processo
Impressor

Impressora
física

Figura 2 ­ Programa concorrente acessando arquivo e impressora.
O uso da programação concorrente é natural nas aplicações que apresentam paralelismo
intrínseco,   ditas  aplicações   inerentemente   paralelas.   Nessas   aplicações   pode­se   distinguir
PROGRAMAÇÃO CONCORRENTE – Prof. Simão Toscani
(Do livro Sistemas Operacionais e Programação Concorrente, Toscani S.S., Oliveira R.S. e Carissimi A.S.
Editora Sagra-Luzzatto, 2003)

2

  o   programa   concorrente   terá   um processo para realizar cada tipo de serviço.  A seguir é considerado um servidor de impressão para uma rede local. Uma forma de programar o servidor de impressão é usar vários processos. PC Usuários PC PC Servidor de Impressão Figura 3 ­ Rede local incluindo um servidor de impressão dedicado.S. Dessa forma. É   importante   observar   que   o   programa   "servidor   de   impressão"   possui   paralelismo intrínseco.. ele passa esse pedaço de arquivo para o processo "Escritor".   (3)   enviar   mensagens   pela   rede   (contendo.S. exemplo que será apresentado a seguir. que as envia através de chamadas de sistema apropriadas. a programação concorrente tem aplicação natural na construção de sistemas que tenham de implementar serviços que são requisitados   de   forma   imprevisível   [DIJ65]. Pode­se dizer que. um arquivo deve ser dividido em várias mensagens para transmissão através da rede. A figura 4 mostra uma das possíveis soluções para a organização interna do programa concorrente   "servidor   de   impressão".   (2)  escrever   em  disco   os  pedaços   de arquivos   recebidos. Oliveira R.facilmente funções para serem realizadas em paralelo.S. É possível que seja necessário a geração e o envio de mensagens de resposta. O processo "Protocolo" gera as mensagens a serem enviadas e passa­as para o processo "Transmissor". Quando o processo "Protocolo" identifica uma mensagem que contém um pedaço de arquivo. (4) ler arquivos  previamente  recebidos  (para imprimí­los). através de variáveis que são compartilhadas pelos processos envolvidos na comunicação. Simão Toscani 3 (Do livro Sistemas Operacionais e Programação Concorrente. Cabe ao processo "Escritor" usar as chamadas de sistema apropriadas para escrever no PROGRAMAÇÃO CONCORRENTE – Prof. Toscani S. (5) enviar dados para a impressora. 2003) . Algumas mensagens contêm pedaços de arquivos a serem impressos.   Cada   círculo   representa   um   processo. e Carissimi A. Ele analisa o conteúdo das mensagens recebidas à luz do protocolo de comunicação suportado pelo servidor de impressão. O processo "Receptor" é responsável por receber mensagens da rede local. cada um responsável por uma atividade em particular. Essa passagem de dados pode ser feita. A figura 3 ilustra uma rede local na qual existem diversos computadores pessoais (PC) utilizados pelos usuários e existe um computador dedicado ao papel de servidor de impressão. Vamos agora descrever a função de cada processo.   Ele   deve:   (1)  receber   mensagens  pela  rede. Obviamente. Editora Sagra-Luzzatto. O servidor usa um disco magnético para manter os arquivos que estão na fila de impressão.   respostas   às consultas  sobre o seu estado).   Nesse   caso. Este é o caso do spooling de impressão. esses processos vão precisar trocar informações para realizar o seu trabalho. em geral. Passa também a identificação do arquivo ao qual o pedaço em questão pertence.   por   exemplo. Ele faz isso através de chamadas de sistema apropriadas e descarta  as mensagens com erro. por exemplo.   Cada   flecha representa a passagem de dados de um processo para o outro. Todas essas atividades podem ser realizadas "simultaneamente". É suposto aqui que o tamanho das mensagens tenha um limite (alguns Kbytes). As mensagens  corretas  são então passadas para o processo "Protocolo".

S. Essa especificação pode ser feita de diversas maneiras. é necessário ter a capacidade de especificar o paralelismo dentro do programa. etc. PROGRAMAÇÃO CONCORRENTE – Prof. 3 Especificação do paralelismo Para construir um programa concorrente. O relacionamento entre os processos "Leitor" e "Escritor" foi descrito antes.S. quit e join [CON63]. Pascal Concorrente.disco. o processo "Escritor" passa para o processo "Leitor" o nome do arquivo. Ada. 2003) 4 . Outras maneiras serão apresentadas na seção 4.  Hoje em dia existem várias linguagens que permitem construir programas concorrentes (Java. o processo "Impressor" é encarregado de enviar os pedaços de arquivo que ele recebe para a impressora. Quando o pedaço de arquivo em questão é o último de seu arquivo. Uma delas utiliza os comandos fork. envia o conteúdo do arquivo para o processo "Impressor" e então remove o arquivo lido.S. C estendido com bibliotecas para concorrência. Toscani S.. que está pronto para ser impresso.   O restante deste capítulo é dedicado aos problemas e às técnicas existentes para a construção de programas concorrentes como esse. Editora Sagra-Luzzatto. O envio do conteúdo para o processo "Impressor" é feito através de um laço interno composto pela leitura de uma parte do arquivo e pelo envio dessa parte. O resultado é uma organização interna clara e simples para   o   programa. Receptor Transmissor Protocolo Escritor Leitor Impressor Figura 4 ­ Servidor de impressão como programa concorrente.   Um   programa   seqüencial   equivalente   seria   certamente   menos   eficiente.). antes de mais nada. Finalmente. O servidor de impressão ilustra o emprego da programação concorrente na construção de uma aplicação com paralelismo intrínseco. no início desta seção. Oliveira R. O processo "Leitor" executa  um laço no qual ele pega um nome de arquivo. Aqui será utilizada uma linguagem apropriada para ensino. Simão Toscani (Do livro Sistemas Operacionais e Programação Concorrente. denominada Vale4 (V4) [TOS04]. e Carissimi A.

Editora Sagra-Luzzatto. Os valores iniciais das variáveis do filho são iguais aos valores das variáveis correspondentes do pai no momento da execução da função fork. onde P é um novo programa para ser executado (novo segmento de código e de dados para o processo). Simão Toscani (Do livro Sistemas Operacionais e Programação Concorrente. Por exemplo. o comando seguinte ao fork tem a seguinte forma: if id = 0 then { processamento do filho } else { processamento do pai } Um dos processos. Um fluxo de execução também é denominado linha de execução ou thread. Isto permite que os processos prossigam de acordo com suas identidades. PROGRAMAÇÃO CONCORRENTE – Prof.S. cria uma cópia do processo original). A thread original (mãe) e a thread criada (filha) executam em paralelo a partir do 1 Basicamente. O processo filho recebe cópias das variáveis do processo pai. a única diferença inicial entre o pai e o filho é o valor da variável id. Na linguagem V4.. porém com uma grande diferença: o fork cria uma thread e não um processo. bem como dos descritores de arquivos. pode “sobrepor” um novo código sobre si. e Carissimi A. Normalmente. O primeiro contém as instruções do processo e o segundo contém as variáveis e constantes do processo. Além disso. Os comandos quit e join são auxiliares ao fork. Em relação aos valores dessas variáveis. Fork. que é 0 para o filho e é o valor de retorno da função fork para o pai. Oliveira R.S. através da operação exec(P). o comando id = fork() cria um filho idêntico ao processo que executou a operação (isto é. join e quit na linguagem Vale4 Na linguagem Vale4. o funcionamento do comando fork é similar ao do Unix. todo processo possui uma pilha que é usada para chamadas e retornos de procedimentos. Quando o comando quit é executado. o processo (ou thread) que o executa termina imediatamente. A função fork retorna um número inteiro que é a identificação do novo processo ou do novo fluxo criado. O valor de retorno é o número de identificação do processo criado (process identification ou pid). a execução do comando id := fork() faz com que a variável global2 id receba o valor retornado pela função fork. por exemplo o filho. Observe que não há compartilhamento de variáveis: as variáveis do pai ficam no espaço de endereçamento1 do pai e as variáveis do filho. As threads mãe e filha são idênticas: executam o mesmo código e compartilham todas as variáveis do processo em que são definidas (é justamente nesse compartilhamento que está a diferença para o Unix).S. o espaço de endereçamento de um processo é formado por um segmento de código e um segmento de dados.O comando (ou função) fork pode ser implementado de duas maneiras distintas: ele pode criar um novo processo ou criar apenas um novo fluxo de execução dentro de um processo já existente. que é o número único da thread criada. Toscani S. Primitiva fork no sistema Unix No sistema operacional Unix a operação fork cria um novo processo que é uma cópia idêntica (clone) do processo que executa esta operação. 2003) 5 . no espaço de endereçamento do filho. O comando join(id) bloqueia quem o executa até que termine o processo (ou thread) identificado por id.

quando se tem um conjunto de threads dentro de um processo. Simão Toscani (Do livro Sistemas Operacionais e Programação Concorrente. que significa o desejo de esperar pelo término de qualquer um dos descendentes imediatos. 2003) 6 .comando que segue ao fork. a linguagem V4 oferece o comando new que será apresentado na seção que segue. todas as threads executam o código do processo e compartilham as suas variáveis. denominada any. O argumento de join pode ser também a palavra reservada any. 2 Variável declarada no processo hospedeiro. Na verdade.S. ao morrer. é uma função primitiva que consulta o registro descritor do processo ou thread para obter o número de sua “carteira de identidade” e retorna esse número. quando se tem um conjunto de processos. 3 PROGRAMAÇÃO CONCORRENTE – Prof. Se desejar pegar esse valor. Nesse caso.S. como função. PSW. Todas as threads de um processo trabalham no mesmo espaço de endereçamento. Diferença entre thread e processo O que melhor distingue uma thread de um processo é o espaço de endereçamento. com um conjunto separado de variáveis. o argumento X é um valor inteiro que o processo ou thread informa ao seu genitor. Toscani S. o comando que segue ao fork tem a seguinte forma: if id = myNumber then { processamento da filha } else { processamento da mãe } Observe que apenas para a thread filha o myNumber é igual ao valor da variável global id. Isto é. myNumber. o genitor usará a primitiva join.5 A propósito. Tudo se passa como se cada processo ou thread tivesse uma variável local. Oliveira R. a variável any4 vai conter a identidade do filho ou filha cuja execução terminou. um quit sem argumento equivale a um quit(0). que é usada para as chamadas e retornos de procedimentos. cada thread possui uma pilha. Por outro lado. que mata o seu executor.). no retorno da primitiva. tudo se passa como se cada processo ou thread tivesse uma variável local. o número único de quem a refere). etc. Tipicamente.S. tem-se um único registro descritor (o registro do processo hospedeiro) e N mini-descritores de threads. cada processo trabalha num espaço de endereçamento próprio. denominada myNumber. O último comando do conjunto é o quit. então um erro é reportado (erro de execução). Usada como função. O valor dessa variável é sempre a identificação de quem a refere (isto é. O comando join(id) permite que um processo (ou thread) P espere pelo término do descendente imediato (filho ou filha) identificado por id. Para criar processos (não threads) dinamicamente. que é a memória lógica do “processo hospedeiro”. que também pode ser referido como myself ou myID. contendo o número único desse processo ou thread. ao morrer. Se o argumento id não identifica um processo ou thread. 5 No caso de ser usada como subrotina. ela retorna o valor que o filho (ou filha) id especificou no comando quit. O mini-descritor de cada thread é usado para salvar os valores dos registradores da UCP (PC. No segundo caso. o comando new cria um novo processo. As duas threads irão se distinguir através da variável denominada myNumber3. Adicionalmente. a primitiva join(id) desconsidera o valor informado pelo descendente id. compartilhada entre mãe e filha. Podem ser usadas as formas quit ou quit(X). Outra peculiaridade da primitiva join(id) é que ela pode ser usada como função ou como subrotina. No caso de um processo com N threads. Editora Sagra-Luzzatto. 4 Semelhantemente ao que ocorre com myNumber. e Carissimi A. ou se id não corresponde a um descendente imediato de P. conforme é explicado a seguir. Enquanto o comando fork cria uma nova thread..

2003) 7 .. O comando write(X) escreve na tela do terminal do usuário. Simão Toscani (Do livro Sistemas Operacionais e Programação Concorrente. f2:= fork(). um elemento de  array  ou um  string  entre apóstrofes. Um exemplo Vamos usar a linguagem V4 para exemplificar o uso dos comandos fork. escreve uma mensagem para cada uma e termina também. nl } end program Figura 5 . write('Filha 2 morreu').  Duas possíveis saídas para o programa anterior seriam: Alo da mae Alo do filha 1 Alo do filha 2 Filha 1 morreu Filha 2 morreu Alo da mae Alo do filha 1 Filha 1 morreu Alo do filha 2 Filha 2 morreu PROGRAMAÇÃO CONCORRENTE – Prof.S.  O  comando  nl (“new line”) faz com que a próxima impressão seja feita em uma nova linha.É mais fácil chavear a execução entre threads (de um mesmo processo) do que entre processos.S. Editora Sagra-Luzzatto. Toscani S. pois tem-se menos informações para salvar e restaurar. as threads são chamadas também de "processos leves". join(f1). /* Cria filha 2 */ if f2 = myNumber then { write('Alo da filha 2'). f1: integer. join e quit. write('Filha 1 morreu'). /* identifica filha 1*/ /* identifica filha 2*/ { write('Alo da mae'). join e quit. quit}. nl. As duas threads criadas apenas colocam uma mensagem na tela e terminam. Por esse motivo. quit}. A linha de execução  inicial  desse processo executa duas vezes o comando  fork. nl. Oliveira R. O programa mostrado na figura 5 possui um  único processo.Uso de comandos fork. O argumento X pode ser uma constante. join(f2).S. f2: integer. /* Cria filha 1 */ if f1 = myNumber then { write('Alo da filha 1'). f1:= fork(). criando duas  threads  adicionais  (filhas 1 e 2). nl. nl. uma variável.  V4program process p1. e Carissimi A. A  thread original então espera que cada uma das filhas termine.

No primeiro caso. em paralelo. Os mecanismos vistos até aqui (fork. Oliveira R. O resultado da execução pode ser qualquer seqüência de tamanho 20. while k < 10 do { write(1). pois os processos (ou threads) são criados somente quando instruções especiais são executadas. as linguagens de programação permitem especificar esses processos de duas maneiras: como processos individuais ou como um array de processos. denominada k. join e quit) realizam criação dinâmica (e término dinâmico). contendo 10 vezes o número 1 e 10 vezes o número 2. PROGRAMAÇÃO CONCORRENTE – Prof. então a thread correspondente não é criada. V4program process P1.4 Criação estática e criação dinâmica de processos Os processos de um programa concorrente podem ser criados de forma estática ou dinâmica. imprime 10 vezes o número 2.. e Carissimi A. embaralhados. Especificação de processos individuais: Neste caso. durante a execução. process P2. se a execução não “passa” por uma determinada instrução fork.S. são possíveis 20!/(10! *10!) resultados diferentes.S. k:=k+1 }. while k < 10 do { write(2). 2003) 8 . Teoricamente. O primeiro imprime 10 vezes o número 1 e o segundo.8 6 Por exemplo. 8 Combinações de uma seqüência de 20 escaninhos. os quais são ativados simultaneamente. as suas variáveis locais e o seu segmento de código. no inicio da execução do programa. denominados P1 e P2. cada processo é especificado de forma individual. k: integer init 0. Normalmente. Editora Sagra-Luzzatto. No segundo caso. k:=k+1 } endprogram O programa define 2 processos. conforme exemplificado a seguir. o programa contém a declaração de um conjunto fixo de processos.6 Criação estática No caso da criação estática. através de instruções especiais para esse fim. para cada processo do programa. os processos são criados dinamicamente. que não compartilham variáveis (não existem variáveis globais no programa). Toscani S. os processos são declarados explícitamente7 no programa fonte e vão existir desde o início da execução do programa concorrente. escolhidos 10 a 10.S. k: integer init 0. Simão Toscani (Do livro Sistemas Operacionais e Programação Concorrente. Cada processo utiliza uma variável local. 7 A declaração explícita consiste em definir.

No caso da linguagem Vale4. conforme é ilustrado a seguir. V4program process type P (i: integer). usada como função ela retorna a identificação interna (pid) do processo criado. considerando o mesmo exemplo anterior. Editora Sagra-Luzzatto. Oliveira R. k:=k+1 }. prontos para serem compilados e PROGRAMAÇÃO CONCORRENTE – Prof. while k < 10 do { write(i). Este programa é equivalente ao anterior. a sua variável local i vale 1 e para o segundo. 5 Exemplos de programas concorrentes Esta seção apresenta programas simples que ilustram características importantes dos programas concorrentes. k: integer init 0. Simão Toscani (Do livro Sistemas Operacionais e Programação Concorrente.Array de processos Neste caso.S. explicita-se um modelo (template) de processo. clones) desse processo durante a execução. e Carissimi A. o qual é utilizado para criar exemplares (cópias.S. Neste caso tem-se o que se denomina criação dinâmica com declaração explícita de processos. a primitiva new pode ser usada como função ou como subrotina. while k < 10 do { write(i). process Q. Criação dinâmica É possível declarar explicitamente um modelo (uma "forma") para criar processos durante a execução. o qual permite passar parâmetros para o processo criado. que é especificada no cabeçalho do processo. uma única declaração especifica um grupo de processos semelhantes..S. A criação de um novo exemplar se dá através do comando new. k: integer init 0. a sua variável local i vale 2. Toscani S. Tratam-se de programas Vale4 completos. que se distinguem apenas pelo valor de uma variável local inteira. new P(2) } endprogram Através da especificação process type. V4program process P (i := 1 to 2). { new P(1). conforme é ilustrado a seguir. 2003) 9 . k:=k+1 } endprogram Para o primeiro processo.

Compartilhamento de uma variável O programa a seguir implementa um sistema concorrente onde dois processos compartilham uma variável global S. Como cada processo (ou thread) trabalha com uma pilha própria. pois cada processo utiliza um conjunto separado de parâmetros e de variáveis locais. cada processo trabalha sobre um conjunto de dados separado. Não se pode esquecer que.) são apresentados em itálico. o resultado da execução desse programa é imprevisível. Editora Sagra-Luzzatto. Cada processo incrementa S de uma unidade. 2003) 10 . process P1.S. process p1. as execuções não interferem uma com a outra.S. sendo 10 possíveis (teoricamente) C 20 resultados distintos. imprime(1). A observação importante é que cada processo utiliza cópias independentes dessas variáveis. os argumentos e as variáveis locais desse procedimento são alocados na pilha do processo chamador. é usada a seguinte notação: os identificadores declarados (variáveis. k:=k+1 }. Cada uma dessas execuções utiliza variáveis i (argumento) e k (variável local do procedimento). Na programação concorrente é sempre assim. Toscani S. todo procedimento é automaticamente reentrável (ou puro). Para melhorar a legibilidade dos programas. 100 vezes. procedimentos. imprime(2) endprogram Cada processo chama imprime fornecendo como argumento o número a ser impresso. Isto significa que o mesmo código pode ser executado simultaneamente por vários processos sem que haja interferência entre eles. Oliveira R. process P2.. Observação sobre as variáveis de um procedimento No exemplo anterior.executados no ambiente V4. uma efetuada por P1 e outra por P2. Como nos exemplos anteriores. na chamada de um procedimento. Compartilhamento de um procedimento O mesmo programa que foi utilizado na seção 3. PROGRAMAÇÃO CONCORRENTE – Prof. O código do procedimento é compartilhado pelos dois processos. Isto é. etc. V4program S : integer init 0. k: integer init 0. Simão Toscani (Do livro Sistemas Operacionais e Programação Concorrente. V4program procedure imprime(i: integer). e Carissimi A. while k < 10 do { write(i). mas os dados (variáveis i e k) são privativos de cada processo.5 para distinguir as diferentes maneiras de especificar (e criar) processos é reescrito para ilustrar o compartilhamento de um procedimento. existem duas execuções concorrentes do procedimento imprime.S.

2003) 11 .S. k: integer init 0. Este comando pode substituir todos os demais comandos iterativos (while. repeat. O comando loop O comando loop implementa um “ciclo infinito”. Observação sobre a inicialização de variáveis Em V4. e Carissimi A. então todos os identificadores da lista recebem esse único valor inicial. Este comando é compilado para a seguinte seqüência de código de máquina: push S % coloca o valor de S na pilha push $1 % coloca a constante 1 na pilha add % soma os dois últimos valores colocados na pilha PROGRAMAÇÃO CONCORRENTE – Prof. o comando “exit when <cond>” significa que o ciclo acaba (isto é. Cada processo executa 100 vezes o comando S:=S+1. as variáveis não explicitamente inicializadas possuem valor inicial igual a 0. Se o uso é na declaração de uma lista de identificadores. os quais são explicados a seguir. todos os elementos do array recebem esse único valor especificado. { loop S:= S+1. tab(2). write('p1').k: integer init 0.. write(S) }.S. process p2. exit when k = 100 endloop. { loop S:= S+1. k:= k+1. k:= k+1. Simão Toscani (Do livro Sistemas Operacionais e Programação Concorrente. for. sendo útil para formatar a impressão de resultados. Editora Sagra-Luzzatto. write('p2'). O comando tab(K) Este comando escreve K espaços em branco no terminal do usuário. write(S) } endprogram Este programa utiliza os comandos loop e tab(K). Toscani S.S. e igual a false. tem-se 2 processos manipulando a variável global S. nl. tab(2). Oliveira R. O problema da exclusão mútua No programa anterior. etc. Quando usada. a cláusula initial (ou init) deve ser seguida de um único valor inicial. No seu interior. em geral. com a vantagem de. tornar os programas mais claros. exit when k = 100 endloop. se inteiras.). nl. Se o uso é na declaração de um array. que a execução vai para o comando seguinte ao endloop) quando a condição <cond> é verdadeira. se booleanas.

então o trabalho é fácil e direto: o escravo movimenta o único disco de a para b (e mostra essa movimentação). Inicialmente os n discos estão empilhados na torre 1. Caso contrário. id:= new Hanoi(m. o outro não pode acessar S. são denominados trechos críticos ou regiões críticas. Oliveira R. id:= new Hanoi(m. c: integer). a. write(' --> '). Para movimentar n discos de a para b.pop S % guarda o resultado em S Como os pontos em que a UCP passa de um processo para outro são imprevisíveis. write(a). pode acontecer de um processo perder a UCP no meio da seqüência acima. descrito a seguir. e Carissimi A. o valor final de S dificilmente será igual a 200. id. c. sem que nunca um disco maior fique sobre um disco menor. m: integer. Como conseqüência. Mais tarde. enquanto um processo estiver manipulando S. como se nada tivesse acontecido. b). menor no topo). o seu estado é salvo. é necessário garantir o acesso exclusivo aos dados compartilhados. a). Os trechos dos processos onde os dados compartilhados são manipulados. O problema consiste em movimentar todos os discos para uma determinada torre. join(id) }. ele vai concluir a seqüência acima e armazenar o valor 11 em S. Nesse caso. deve haver exclusão mútua no acesso à variável S. Todos os acréscimos a S feitos pelo outro processo nesse ínterim são perdidos.S. usando b como torre intermediária. a.S. 9 Sempre que um processo perde a UCP. quando os dados não são alterados pelos processos. quando este processo receber a UCP de volta.. join(id). um escravo Hanoi faz o seguinte. A exclusão mútua só não é necessária quando os dados são compartilhados na modalidade "apenas leitura". Se n = 1 (só tem um disco para movimentar). a execução do processo continua. PROGRAMAÇÃO CONCORRENTE – Prof. if n = 1 then { nl. o escravo cria um escravo_1 para movimentar n-1 discos de a para c. Quando esse serviço é concluído (escravo_1 termina o seu trabalho). o escravo original movimenta o disco que lhe sobrou (que é o maior) de a para b (mostra essa movimentação) e cria um escravo_2 para concluir o serviço. Para o resultado ser 200. write(a). Toscani S. Os discos devem ser transportados de um em um e a terceira torre pode ser usada como intermediária nessas movimentações. usando c como torre intermediária. Em geral. b.S. usando a torre 3 como intermediária. Criação dinâmica de processos A criação dinâmica (e recursiva) de processos é ilustrada através do problema da torre de Hanói. A exclusão mútua é um requisito muito importante nos sistemas concorrentes. embora S inicie com o valor zero e cada processo some 1 a S cem vezes. isto é.9 Vamos supor que o valor de S carregado na pilha seja 10 e que o processo perca a UCP. write(' --> '). que é movimentar n-1 discos de c para b usando a como torre intermediária. write(b). Tem-se 3 torres (pilhas) e n discos de tamanhos diferentes. O programa inicia com o processo P criando um filho (escravo) Hanoi para movimentar 3 discos da torre 1 para a torre 2. write(b) } else { m:= n-1. 2003) 12 . Editora Sagra-Luzzatto. na ordem certa (maior na base. V4program process type Hanoi(n. b. Simão Toscani (Do livro Sistemas Operacionais e Programação Concorrente. c. nl. isto é.

. os comandos lock e unlock não formam uma estrutura sintática (isto é. inclusive. respectivamente. 10 O recurso pode ser. eles são comandos independentes. P e V são as iniciais das palavras holandesas Proberen e Verhogen. respectivamente. como se fossem um abre e fecha parênteses). Se wakeup(P) é executado antes. é deixada aberta). Oliveira R. as operações P e V têm a seguinte semântica: • P(S) : espera até S ser maior que 0 e então subtrai 1 de S. unlock Um processo só entra num trecho delimitado pelo par lock/unlock se nenhum outro processo está executando em um outro trecho delimitado dessa maneira. Simão Toscani (Do livro Sistemas Operacionais e Programação Concorrente.process P. Este último comando acorda (desbloqueia) o processo especificado por P. block/wakeup(P) Quando um processo P executa o comando block. 6 Sincronizações básicas É comum um processo ter que esperar até que uma condição se torne verdadeira. descritas a seguir. Na verdade. a fechadura é destrancada (isto é. não precisam estar casados.10 2. "o direito de acessar os dados compartilhados". 2003) 13 . .S.S. o primeiro processo que executa o comando lock passa e tranca a passagem (chaveia a fechadura) para os demais. O comando unlock deixa passar (desbloqueia) o primeiro processo da fila de processos que estão bloqueados por terem executado um lock (enquanto a fechadura estava trancada). Dois tipos de bloqueio são considerados básicos: 1. Estes bloqueios são caracterizados pelas operações básicas lock/unlock e block/wakeup(P). 1.11 Sendo S é uma variável semáfora.S. 3) endprogram O resultado da execução deste programa será a ordem em que os discos deverão ser movimentados (movimento por movimento. Toscani S. bloquear até que um recurso se torne disponível. Na linguagem Vale4. Para essa espera ser "eficiente". Editora Sagra-Luzzatto.. e Carissimi A. as operações lock e unlock também podem ser referidas pelos nomes mutexbegin e mutexend. o processo P não se bloqueia ao executar o block. denominadas P e V. 7 Semáforos As variáveis semáforas são variáveis especiais que admitem apenas duas operações. ele se bloqueia até que um outro processo execute o comando wakeup(P). o argumento de wakeup pode ser um nome de processo ou um número único de processo ou thread. um disco por vez). bloquear até que chegue um sinal de outro processo. que significam testar e incrementar. 2. . Na linguagem Vale4. o segundo par implementa uma forma básica de comunicação. Enquanto o primeiro par (lock/unlock) implementa sincronização do tipo exclusão mútua. Se a fila está vazia. o processo deve esperar no estado bloqueado (sem competir pela UCP). new Hanoi(3. . 11 PROGRAMAÇÃO CONCORRENTE – Prof. Isto é. lock.

P(X)..... REGIÃO CRÍTICA. Sincronizações básicas com semáforos As variáveis semáforas permitem implementar os dois tipos básicos de bloqueio.. B. Editora Sagra-Luzzatto. 2003) 14 .. se nenhuma está disponível. Simão Toscani (Do livro Sistemas Operacionais e Programação Concorrente..S. programa-se da seguinte maneira: Y : semaphore initial 0 P1: .. .• V(S) : incrementa S de 1.S.. V(X). O valor numérico do semáforo corresponde ao número de bolitas dentro do vaso. Sincronização tipo block/wakeup(P): Este tipo de sincronização é implementado através de um semáforo com valor inicial zero.S. . % block . P(Y). A... um dos processos vai ficar bloqueado... .. REGIÃO CRÍTICA. % wakeup(P1) . conforme é explicado a seguir. . O acesso exclusivo a n regiões críticas seria implementado como segue: X : semaphore initial 1 P1: .. Toscani S. Cada operação P tenta remover uma bolita.... V(Y). As operações “testar S e subtrair 1 (se S > 0)” e “incrementar S de 1” são executadas de forma atômica (indivisível). Uma operação V corresponde a pôr uma bolita no vaso. Sincronização tipo lock/unlock: A sincronização do tipo exclusão mútua é implementada através de um semáforo com valor inicial 1. se a operação B do processo P1 deve ser executada após a operação A do processo P2. Quando uma bolita é colocada no vaso (operação V). V(X).. .. ela é removida pelo primeiro processo da fila de espera. Oliveira R... REGIÃO CRÍTICA. P(X).. Pn: . PROGRAMAÇÃO CONCORRENTE – Prof. o qual prossegue sua execução. P(X). ... e Carissimi A. V(X). Se S = 1 e dois processos executam P(S) “simultaneamente”. P2: . então a operação bloqueia o processo e o coloca numa fila de espera. no kernel do SO.. Por exemplo.. Pode-se fazer analogia entre uma variável semáfora e um vaso contendo bolitas (bolinhas de gude). P2: .

buffer[in]:= msg. o produtor deve se bloquear. as variáveis inteiras que não são inicializadas tem seu valor inicial igual a zero. Simão Toscani (Do livro Sistemas Operacionais e Programação Concorrente. cheios: semaphore initial 0. in:= (in mod 5)+1. Isto porque os dois processos trabalham com variáveis locais in. A relação produtor-consumidor ocorre comumente em sistemas concorrentes e o problema se resume em administrar o buffer que tem tamanho limitado.8 Programas clássicos Esta seção apresenta 4 problemas clássicos da programação concorrente. loop P(cheios). Se o buffer está cheio. A programação desse sistema com buffer de 5 posições e supondo que as mensagens sejam números inteiros. loop % produz mensagem msg P(vazios). certamente. Cada um possui seu lugar numa mesa circular. em cujo centro há um grande prato de spaghetti. msg:= buffer[out]. Variáveis globais: buffer: array[5] of integer. out:= (out mod 5)+1. vazios: semaphore initial 5. Oliveira R. se o buffer está vazio.. Conforme já foi referido. O segundo processo. Toscani S.S.S. out e msg e. e Carissimi A. ela requer dois garfos para ser PROGRAMAÇÃO CONCORRENTE – Prof. Produtor-consumidor com buffer limitado Este problema pode ser enunciado como segue. 2003) 15 . Observe que a solução não se preocupou em garantir exclusão mútua no acesso ao buffer. % consome a mensagem endloop O semáforo cheios conta o número de buffers cheios e o semáforo vazios conta número de buffers vazios. o consumidor deve se bloquear. in : integer. irão acessar sempre posições diferentes do vetor global buffer. denominado produtor. Um par de processos compartilha um buffer de N posições. Como a massa é muito escorregadia. Editora Sagra-Luzzatto. V(cheios) endloop Processo consumidor: msg. O primeiro processo. passa a vida a retirar mensagens do buffer (na mesma ordem em que elas foram colocadas) e a consumí-las. é mostrada a seguir. Processo produtor: msg. Existem N filósofos que passam suas vidas pensando e comendo. denominado consumidor. passa a vida a produzir mensagens e a colocá-las no buffer. V(vazios). out : integer. Jantar dos Filósofos Este problema ilustra as situações de deadlock e de postergação indefinida que podem ocorrer em sistemas nos quais processos adquirem e liberam recursos continuamente. A figura 6 ilustra a situação para 5 filósofos. todos resolvidos através do uso de semáforos e todos escritos de acordo com a sintaxe de Vale4.S.

comida. depois o da direita. V(garfo[i]) } }. com exceção de um deles.S. Toscani S. Simão Toscani (Do livro Sistemas Operacionais e Programação Concorrente. PROGRAMAÇÃO CONCORRENTE – Prof. P(garfo[i]) } }.S. process filosofo (i:= 1 to 5). O programa a seguir é um programa Vale4 completo. Foi escolhido o filósofo 1 para ser do contra. e Carissimi A. um entre cada dois filósofos. Na solução a seguir. filósofo 3 garfo 3 filósofo 4 garfo 2 filósofo 2 spaghetti garfo 1 garfo 4 filósofo 1 filósofo 5 garfo 5 Figura 6 . P(garfo[5]) } else { P(garfo[j]). no máximo. Editora Sagra-Luzzatto. V4program garfo: array[5] of semaphore init 1. Oliveira R.S. { j := i-1 . O problema consiste em simular o comportamento dos filósofos procurando evitar situações de deadlock (bloqueio permanente) e de postergação indefinida (bloqueio por tempo indefinido). no qual cada filósofo faz 10 refeições e morre. Pode ser demonstrado que esta solução é livre de deadlocks.. V(garfo[5]) } else { V(garfo[j]). procedure putForks(i: integer). Os garfos são representados por um vetor de semáforos e é adotada a seguinte regra: todos os 5 filósofos pegam primeiro o seu garfo da esquerda. % j é o garfo da esquerda if j = 0 then { P(garfo[1]). j: integer. % j é o garfo da esquerda if j = 0 then { V(garfo[1]). Na mesa existem N garfos. 2003) 16 . % array global procedure getForks(i: integer). e os únicos garfos que um filósofo pode usar são os dois que lhe correspondem (o da sua esquerda e o da sua direita). iremos nos concentrar no caso de 5 filósofos. j: integer. { j := i-1 . N/2 filósofos poderão estar comendo de cada vez.A mesa dos 5 filósofos Da definição do problema. tem-se que nunca dois filósofos adjacentes poderão comer ao mesmo tempo e que. que é do contra.

S. while k > 0 do { getForks(i). o barbeiro senta numa cadeira e dorme. ele libera mutex e vai embora sem cortar o cabelo. write(‘ parou de comer’). ele ocupa uma cadeira e espera (se tem alguma cadeira disponível) ou vai embora (se todas as cadeiras estão ocupadas). PROGRAMAÇÃO CONCORRENTE – Prof. count. nl. O barbeiro adquire a exclusão mútua. Se tem alguma cadeira disponível.S. Editora Sagra-Luzzatto. A barbearia tem uma sala de espera com N cadeiras e uma cadeira de barbear. Variáveis globais: clientes. O semáforo clientes tranca o barbeiro. A solução é apresentada a seguir. A seguir. o cliente espera. ele acorda o barbeiro. ele vai embora. O semáforo fila tranca os clientes e implementa a fila de espera. O semáforo mutex garante exclusão mútua. sendo suas “bolitas” produzidas pelos clientes que chegam. Se não tem clientes à espera. putForks(i). caso contrário. Oliveira R. fila e mutex. o barbeiro executa a operação P(clientes). Outro cliente que chegar imediatamente após. o cliente verifica se o número de pessoas à espera é menor ou igual ao número de cadeiras. O valor desse semáforo indica o número de clientes à espera (excluindo o cliente na cadeira do barbeiro. considerando o número de cadeiras na sala de espera igual a 3. A solução a seguir usa 3 semáforos: clientes. e Carissimi A. o cliente incrementa a variável count e executa a operação V no semáforo clientes. nl. fila: semaphore init 0. write(i). Quando chega um cliente. write(‘filosofo ’). count : integer initial 0. ele começa adquirindo a exclusão mútua. mutex: semaphore init 1.. Toscani S. é adicionada uma “bolita” no semáforo clientes. que não está à espera). k:=k-1 } endprogram Barbeiro dorminhoco O problema consiste em simular o funcionamento de uma barbearia com as seguintes características. Se chega outro cliente enquanto o barbeiro está trabalhando. que conta o número de clientes à espera. Dentro da região crítica. Inicialmente. O valor desta variável é sempre igual ao “número de bolitas” do semáforo clientes. ele é acordado. Geral da acacao de sert Um cliente que chega na barbearia verifica o número de clientes à espera. write(‘filosofo ’). decrementa o número de clientes. Se esse número é menor que o número de cadeiras. Simão Toscani (Do livro Sistemas Operacionais e Programação Concorrente.k: integer init 10. Quando chega um cliente. irá se bloquear até que o primeiro libere a exclusão mútua. 2003) 17 .S. write(i). Se não é. pega o primeiro da fila de espera e vai fazer o corte. Também é usada uma variável inteira. onde fica bloqueado (dormindo) até a chegada de algum cliente. write(‘ comecou a comer’). caso contrário. o cliente libera a exclusão mútua e entra na fila de espera. Se o barbeiro está dormindo.

Processo cliente: P(mutex).S. V(clientes). Oliveira R. e Carissimi A. o barbeiro faz outro corte.S. /*dorme. PROGRAMAÇÃO CONCORRENTE – Prof. /*acorda o barbeiro*/ V(mutex). o barbeiro dorme. Toscani S. Editora Sagra-Luzzatto.S. P(fila). if count < 3 then { count:= count+1. se for o caso*/ P(mutex). /*pega próximo cliente*/ V(mutex). V(fila). Se tem cliente. count:= count –1. Simão Toscani (Do livro Sistemas Operacionais e Programação Concorrente.Processo barbeiro: Loop P(clientes).. /*espera o barbeiro*/ /*corta o cabelo*/ Endloop /*corta o cabelo*/ } else V(mutex) Quando termina o corte de cabelo. o cliente deixa a barbearia e o barbeiro repete o seu loop onde tenta pegar um próximo cliente. 2003) 18 . Se não tem.

e uma variável inteira nr. Na solução a seguir é dada prioridade para os processos readers.. PROGRAMAÇÃO CONCORRENTE – Prof. P(w). V(mutex)... qualquer processo writer deve ter acesso exclusivo ao arquivo. Simão Toscani (Do livro Sistemas Operacionais e Programação Concorrente.S. V(mutex). WRITE V(w). Enquanto houver reader ativo. Variáveis globais: mutex. Processo leitor: .. processos leitores.. nr:=nr-1. .S. w : semaphore initial 1. .. . Editora Sagra-Luzzatto.. if nr=0 then V(w). para contar o número de processos leitores ativos.. P(mutex).... Este problema surge quando processos executam operações de leitura e de atualização sobre um arquivo global (ou sobre uma estrutura de dados global). mutex e w. Note que o primeiro reader bloqueia o progresso dos writers que chegam após ele. São utilizadas duas variáveis semáforas. Entretanto. para exclusão mútua. os writers ficarão bloqueados. if nr=1 then P(w).Leitores e escritores O problema dos readers and writers [COU71] ilustra outra situação comum em sistemas de processos concorrentes. . que não alteram a informação) possam utilizar o arquivo simultaneamente.S. READ P(mutex). nr:=nr+1. Toscani S. Oliveira R. 2003) 19 .. através do semáforo w. e Carissimi A. A sincronização deve ser tal que vários readers (isto é. Processo escritor: . nr : integer initial 0..

Oliveira R. Usando semáforos.EXERCÍCIOS 1... 6. Editora Sagra-Luzzatto. process P (i := 1 to 5). write('B'). Explique por que a solução apresentada no texto para o problema do produtor-consumidor não suporta múltiplos processos produtores e múltiplos processos consumidores. } endprogram 4. .. Considere a instrução TS(X.... cada processo só pode imprimir 'B' depois de todos os outros terem impresso 'A'). de maneira que o resultado impresso seja sempre "AAAAABBBBB" (isto é. . complete o programa Vale4 a seguir.. Um semáforo binário é um semáforo cujo valor pode ser 0 ou 1. 3. .S. onde pi (1  i  6) imprime 10 vezes o número i. PROGRAMAÇÃO CONCORRENTE – Prof.S. escreva um programa formado por 6 processos p1.. p2..S.L) que executa o seguinte código de forma indivisível: if X then go to L else X:=true. Mostre como implementar as operações P e V sobre um semáforo binário usando a instrução TS. 2003) 20 . { . Implemente uma solução para o problema do buffer limitado.. e Carissimi A. Simão Toscani (Do livro Sistemas Operacionais e Programação Concorrente.. Os processos devem estar sincronizados de acordo com o grafo de fluxo de processos abaixo: I p1 p4 p5 p2 p3 p6 F 5. write('A'). . Usando a linguagem V4 e variáveis semáforas. 2.. considerando um processo produtor e vários consumidores. Toscani S. p6. V4program ... Mostre como um semáforo geral pode ser implementado a partir de semáforos binários.

} 8. Simão Toscani (Do livro Sistemas Operacionais e Programação Concorrente.7. PROGRAMAÇÃO CONCORRENTE – Prof. process produtor.S... V( espera_dado ). Crie uma seqüência de eventos que termina em algum comportamento indesejado para o programa. P( exclusao_mutua ).. exclusao_mutua: semaphore init 1. { . buffer: array [N] of integer. proxima_insercao := ( proxima_insercao + 1 ) % N.. P( espera_dado ). buffer[ proxima_insercao ] := dado_produzido. espera_vaga: semaphore init N. espera_dado: semaphore init 0... P( espera_vaga ). proxima_remocao := ( proxima_remocao + 1 ) % N. Descreva o erro na implementação do produtor-consumidor mostrada abaixo. Editora Sagra-Luzzatto. dado_a_consumir := buffer[ proxima_remocao ]. V( espera_vaga ). process consumidor. . 2003) 21 . proxima_remocao: integer init 0. proxima_insercao. }... P( exclusao_mutua ). Simplifique a solução para o problema do produtor-consumidor para o caso de existir apenas uma vaga disponível no buffer circular.S. Toscani S. V( exclusao_mutua ). V( exclusao_mutua ).S. { . . e Carissimi A. Oliveira R..

R. ACM 14. D. 549-557. T. AFIPS Fall Joint Computer Conference.21-28. 2003. PROGRAMAÇÃO CONCORRENTE – Prof. Editora Sagra-Luzzatto. Kit de instalação da linguagem VALE4. J. 1995. No. Concurrent Control with “Readers” and “Writers”. A. No. 1974. S. Genuys. HEYMANS. pp. S.. 6. S. No. W. 1984. ed.br/~stoscani/V4. 2004 (3ª edição). Comm. [HOA74] HOARE. 2. Série didática do II-UFRGS. Erratum in Comm. CARISSIMI. A. TOSCANI. [SHA84] SHATZ. DUFF. R. 2003) 22 . page 95 (January) 1975.and PARNAS. ACM 17. e Carissimi A. R. R. Proc. Comm. 1963. S. 1971. 667-668. [TAF97] TAFT.BIBLIOGRAFIA [CON63] CONWAY.inf. OLIVEIRA. E. L. M.S. (Lecture Notes in Computer Science. Las Vegas. Monitors: An Operating System Structuring Concept.. [COU71] COURTOIS. New York. S. 43-112. Série didática do II-UFRGS. Oliveira R. Technological University. CARISSIMI. C.) pp. Academic Press. P. Cooperating Sequential Processes. pp.. Sistemas Operacionais. 1968. F. Toscani S. S.pucrs. Eindhoven. 10 (October). Computer 17. Reprinted in: Programming Languages (F. 10 (October). A. Vol 1246). ADA 95 Reference Manual: language and standard libraries. M.. E. 1965. Nevada. ACM 18. [OLI04] OLIVEIRA. Sistemas Operacionais e Programação Concorrente.139-146.. Springer-Verlag. A. [TOS04] TOSCANI. A Multiprocessor system design. [TOS03] TOSCANI. No.. Disponível em www. Communication mechanisms for programming distributed systems. Simão Toscani (Do livro Sistemas Operacionais e Programação Concorrente. Technical Report EWD-123.S. [DIJ65] DIJKSTRA. the Netherlands.S.