Buscar

02 - Fundamentos de Computação Paralela

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes
Você viu 3, do total de 89 páginas

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes
Você viu 6, do total de 89 páginas

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes
Você viu 9, do total de 89 páginas

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes

Prévia do material em texto

DESCRIÇÃO
Conceitos introdutórios de programação paralela, variáveis compartilhadas,
sincronização de processos, semáforos, monitores e ambientes de
programação.
PROPÓSITO
Compreender os conceitos fundamentais de programação paralela e as
técnicas inerentes a esse ambiente, para permitir ao aluno o desenvolvimento
de aplicações executadas em ambientes distribuídos.
OBJETIVOS
MÓDULO 1
Identificar problemas e soluções para o uso de variáveis compartilhadas
MÓDULO 2
Reconhecer condições de corrida e recursos utilizados para tratamento
MÓDULO 3
Distinguir as diferentes abstrações de programação, como semáforos,
monitores e suas aplicações
MÓDULO 4
Empregar diferentes ambientes de programação, em especial corrotinas,
Pthreads, OpenMP e Erlang
INTRODUÇÃO
O desenvolvimento dos sistemas distribuídos nos permitiu acessar diversos
tipos de aplicações, disponibilizadas na internet. Essas aplicações mudaram a
forma de estudarmos, de trabalharmos, de nos divertimos e até a maneira
como as pessoas se relacionam.
Uma aplicação distribuída, porém, tem algumas complexidades. Por ser
distribuída, existem vários processos que vão compô-la e, consequentemente,
há a necessidade de uma coordenação entre esses processos.
Vários processos sendo executados simultaneamente dão origem ao conceito
de programação paralela − divisão de uma tarefa computacional em instâncias
(processos) independentes e que podem ser executados de forma paralela.
Os processos que são executados em paralelo, no entanto, podem trazer
algumas situações inconvenientes durante sua execução. Imagine, por
exemplo, que existe um recurso − como uma impressora − que deve ser
acessado por esses processos que estão executando em paralelo. Se todos
quiserem acessar simultaneamente, teremos problemas para definir quem irá
imprimir primeiro.
É necessário conhecer os fundamentos da computação paralela para entender
os problemas que existem nesse paradigma computacional e quais
mecanismos podemos utilizar para evitar ou, pelo menos, minimizar os seus
efeitos.
Por fim, conheceremos diversas plataformas que podem ser utilizadas para o
desenvolvimento de programas paralelos.
Antes de iniciar o módulo 1, assista ao vídeo a seguir da entrevista com o
Professor Jairo Panetta. Ele aborda o que é computação paralela, assim como
o seu histórico e sobre onde é possível atuar no mercado.
MÓDULO 1
 Identificar problemas e soluções para o uso de variáveis
compartilhadas
VARIÁVEIS COMPARTILHADAS
Antes de mais nada, saiba que:
Em programas de memória compartilhada, as variáveis podem ser
compartilhadas ou privadas.
Em um ambiente onde um processo possui vários threads, as variáveis
compartilhadas podem ser lidas ou escritas por qualquer um deles, e variáveis
privadas normalmente podem ser acessadas apenas por um thread. Quando a
comunicação entre os threads é feita por meio de variáveis compartilhadas, ela
é implícita, não explícita − como por soquetes ou named pipes.
 COMENTÁRIO
Em muitos ambientes, os programas de memória compartilhada usam threads
dinâmicos. Nesse modelo, geralmente há um thread mestre e, em qualquer
momento, uma coleção (possivelmente vazia de início) de threads de trabalho.
O thread mestre normalmente espera por solicitações de trabalho − por
exemplo, em uma rede −, e, quando um novo pedido chega, ele bifurca/inicia
um thread de trabalho.
Esse thread executa a solicitação e, quando conclui o trabalho, termina e se
junta ao thread mestre. Esse tipo de modelo faz uso eficiente dos recursos do
sistema, uma vez que os recursos exigidos por um thread só estão sendo
usados enquanto este está realmente em execução.
PARADIGMA DE THREAD ESTÁTICO
Uma alternativa ao paradigma dinâmico é o paradigma de thread estático.
Nesse paradigma, todos os threads são bifurcados/iniciados após qualquer
configuração necessária pelo thread mestre e os threads são executados até
que todo o trabalho seja concluído. Depois que os threads se juntam ao thread
mestre, este pode fazer alguma limpeza (por exemplo, liberação de memória)
e, então, também finaliza esses threads.
 ATENÇÃO
Em termos de uso de recursos, isso pode ser menos eficiente: se um thread
estiver ocioso, seus recursos (por exemplo: pilha, contador de programa etc.)
não podem ser liberados.
No entanto, bifurcar e unir threads podem ser operações bastante demoradas.
Portanto, se os recursos necessários estiverem disponíveis, o paradigma de
thread estático tem potencial de melhor desempenho do que o paradigma
dinâmico. Ele também tem a virtude de estar mais próximo do paradigma mais
amplamente usado para programação de memória distribuída, então, parte da
memória que é usada para um tipo de sistema é preservada para o outro.
Assim, o paradigma de thread estático é o mais frequente.
CÁLCULO NÃO DETERMINÍSTICO
Em qualquer sistema MIMD, no qual os processadores são executados de
forma assíncrona, é provável que haja não determinismo das tarefas. Um
cálculo é não determinístico se uma entrada específica pode resultar em
saídas diferentes. Se vários threads estiverem executando de forma
independente, a taxa relativa na qual eles concluirão as instruções varia de
execução para execução e, portanto, os resultados do programa podem ser
diferentes a cada execução.
 EXEMPLO
Suponha que temos dois threads, um com id 0 e o outro com id 1. Suponha
também que cada um está armazenando uma variável privada x, o valor de x
na thread 0 é 5 e na thread 1 é 8. Além disso, imagine que ambos os threads
executem o seguinte código:
printf("Thread %d > val = %dnn", x);
Então, a saída pode ser:
Thread 0 > val = 5 
Thread 1 > val = 8
Mas pode ser também:
Thread 1 > val = 8
Thread 0 > val = 5
Na verdade, as coisas podem ser ainda piores: a saída de um thread pode ser
interrompida pela saída de outro thread. No exemplo anterior, estamos apenas
tratando de uma saída.
No entanto, como os threads estão executando de forma independente e
interagindo com o sistema operacional, o tempo que leva para um thread
completar um bloco de instruções varia de execução para execução. Portanto,
a ordem em que essas instruções são concluídas não pode ser prevista.
 COMENTÁRIO
Em muitos casos, o não determinismo não é um problema. Em nosso
exemplo, uma vez que rotulamos a saída com a classificação do tópico, a
ordem em que a saída aparece provavelmente não importa. No entanto,
também existem muitos casos em que o não determinismo, especialmente em
programas de memória compartilhada, pode ser desastroso, porque pode
facilmente resultar em erros de programa.
Vejamos um exemplo simples com dois threads: suponha que cada thread
calcule um valor inteiro, que ele armazena em uma variável privada val, e que
queremos adicionar os valores armazenados em val em um local de memória
compartilhada x que foi inicializado em 0. Ambos os threads, portanto,
desejam executar um código semelhante a este:
val = Compute_val(my_rank); 
x += val;
Agora, lembre-se de que uma adição normalmente requer o carregamento dos
dois valores a serem adicionados aos registradores, a adição dos valores e,
por fim, o armazenamento do resultado. Para manter o exemplo relativamente
simples, vamos presumir que os valores são carregados da memória principal
diretamente nos registradores e armazenados na memória principal
diretamente dos registradores. Na tabela a seguir, visualizamos uma
sequência possível de execução desses threads utilizando os valores 5 e 8 do
exemplo anterior:
Tempo Núcleo 1 Núcleo 2
0 Carrega o valor de val
Chama a função
Compute_val
1
Carrega x = 0 para o
registrador
Carrega o valor de val
2
Carrega val = 5 para o
registrador
Carrega x = 0 para o
registrador
3 Adiciona val a x
Carrega val = 8 para o
registrador
4 Armazena x = 5 Adiciona val a x
5 Inicia outro trabalho Armazena x = 8
 Atenção! Para visualização completa da tabela utilize a rolagem horizontal
 Tabela: Saída de eventos de dois threads distintos.Elaborada por: Sergio Kostin
Claramente, não é isso que queremos, pois o resultado deveria considerar que
estamos incrementando o valor de x, e o resultado esperado deveria ser 13.
Assim, é fácil imaginar outras sequências de eventos que resultam em um
valor incorreto para x.
O não determinismo aqui é resultado do fato de que duas threads estão
tentando atualizar mais ou menos simultaneamente à localização da memória
x.
Quando threads ou processos tentam acessar simultaneamente um recurso, e
os acessos podem resultar em erro, costumamos dizer que o programa tem
uma condição de corrida, porque os threads ou processos estão em uma
“corrida de cavalos”. Ou seja, o resultado do cálculo depende de qual thread
ganha a corrida. Em nosso exemplo, os threads estão em uma corrida para
executarx + = val. Nesse caso, a menos que um encadeamento conclua x + =
val antes do outro encadeamento iniciar, o resultado será incorreto.
 VOCÊ SABIA
Um bloco de código que só pode ser executado por um thread de cada vez é
chamado de seção crítica (veremos isso em mais detalhes adiante), e,
geralmente, é nosso trabalho como programadores garantir acesso
mutuamente exclusivo à seção crítica. Em outras palavras, precisamos
garantir que, se um encadeamento estiver executando o código na seção
crítica, os outros encadeamentos serão excluídos.
MUTEX
O mecanismo mais comumente utilizado para garantir a exclusão mútua é um
bloqueio de exclusão mútua ou mutex ou bloqueio.
 ATENÇÃO
A ideia básica é que cada seção crítica seja protegida por um “tipo de
fechadura”. Antes que um thread possa executar o código na seção crítica, ele
deve "obter" essa fechadura, no caso o mutex, chamando uma função mutex
e, quando terminar de executar o código na seção crítica, deve "abandonar" o
mutex, chamando uma função de desbloqueio.
Enquanto um thread "possui" o bloqueio − isto é, retornou de uma chamada
para a função de bloqueio, mas ainda não chamou a função de desbloqueio −,
qualquer outro thread que tentar executar o código na seção crítica aguardará
em sua chamada para a função de bloqueio.
Para garantir que nosso código funcione corretamente, podemos modificá-lo
para que se pareça com isto:
val = Compute_val(my_rank); 
Lock(&add my_val_lock); 
x += val; 
Unlock(&add my_val_lock);
Assim, garante-se que apenas um thread de cada vez possa executar a
instrução x + = val. Observe que o código não impõe nenhuma ordem
predeterminada nos threads que executam o código. Tanto o thread 0 quanto o
thread 1 podem executar x + = val primeiro.
Observe também que o uso de um mutex impõe a serialização da seção
crítica. Como apenas um thread por vez pode executar o código na seção
crítica, esse código é efetivamente serial. Portanto, queremos que nosso
código tenha o menor número possível de seções críticas, e que nossas
seções críticas sejam as mais curtas possíveis.
Existem alternativas para seções críticas além do mutex. Na espera ocupada,
por exemplo, um thread entra em um loop cujo único propósito é testar uma
condição.
Suponha que haja uma variável compartilhada ok que foi inicializada como
falsa. Então, algo como o código a seguir pode garantir que o thread 1 não
atualize x até que o thread 0 o tenha atualizado:
val = Compute_val(my_rank); 
while (ok != 1); // Espera ocupada 
ok = 0 // Bloqueia a entrada de outros thread 
x += my val; // Executa a seção crítica 
ok = 1 // Desbloqueia a variável
Até que o thread 0 seja executado, a variável ok passa para o valor 0,
bloqueando a entrada de outros threads. Assim, o thread 1 ficará preso no
loop while (ok != 1); − chamado de "espera ocupada", porque o thread pode
estar muito ocupado esperando a condição.
 SAIBA MAIS
Simples de entender e implementar, isso, no entanto, pode ser um grande
desperdício de recursos do sistema, porque mesmo quando um
encadeamento não está fazendo nenhum trabalho útil, o núcleo que o executa
verifica repetidamente se a seção crítica pode ser acessada.
MEMÓRIA COMPARTILHADA
Tratamos, até aqui, do uso simplificado de memória compartilhada utilizando
threads dentro de um mesmo processo. Entretanto, é possível fazer o uso de
memória compartilhada usando processos diferentes, como você pode
observar na imagem a seguir:
 
Imagem: Sergio Kostin
 Memória compartilhada.
Sabemos que, para nos comunicarmos entre dois ou mais processos, usamos
memória compartilhada. Mas, antes de usar a memória compartilhada, o que
precisa ser feito com as chamadas de sistema, tendo uma sequência de
operações para isso?
1
Devemos criar o segmento de memória compartilhada. No Linux, isso pode ser
feito com a chamada .
Devemos, posteriormente, anexar o segmento de memória anteriormente
criado ao nosso programa com a chamada .
2
3
Fazemos nossas operações sobre a memória, como escrita e leitura, com a
chamada .
Vale ressaltar que devemos ter todos os cuidados necessários, decorrentes da
programação concorrente, citados neste conteúdo.
4
shmget ()
shmget ()
shmget ()
5
Por fim, devemos desanexar o segmento de memória compartilhada de nosso
programa com a chamada .
O uso de memória compartilhada em processos distintos também está
disponível nos diversos sistemas operacionais, como, por exemplo, o
Windows.
MEMÓRIA COMPARTILHADA
shmget ()
Para finalizar o primeiro módulo, assista ao vídeo a seguir sobre o conceito de
memória compartilhada e as soluções que podem ser utilizadas para o uso
desse tipo de memória.
VERIFICANDO O APRENDIZADO
1. CONSIDERANDO OS CONCEITOS DE VARIÁVEIS
COMPARTILHADAS APRESENTADOS, EM RELAÇÃO ÀS
AFIRMAÇÕES A SEGUIR, MARQUE A ALTERNATIVA
CORRETA. 
 
I. O PARADIGMA DE THREAD ESTÁTICO POSSIBILITA QUE
PARTE DA MEMÓRIA QUE É USADA PARA UM TIPO DE
SISTEMA SEJA PRESERVADA PARA OUTRO. 
II. NO PARADIGMA DE THREAD DINÂMICO, TODOS OS
THREADS SÃO BIFURCADOS/INICIADOS APÓS QUALQUER
CONFIGURAÇÃO NECESSÁRIA PELO THREAD MESTRE, E
OS THREADS SÃO EXECUTADOS ATÉ QUE TODO O
TRABALHO SEJA CONCLUÍDO. 
III. SE OS RECURSOS NECESSÁRIOS JÁ ESTIVEREM
DISPONÍVEIS, É MAIS ADEQUADO O USO DO PARADIGMA
DE THREAD ESTÁTICO, EM VEZ DO THREAD DINÂMICO. 
A) Apenas a afirmação I está correta.
B) As afirmações I e II estão corretas.
C) As afirmações I e III estão corretas.
D) As afirmações II e III estão corretas.
E) Apenas a afirmação III está correta.
2. CONSIDERANDO OS CONCEITOS DE VARIÁVEIS
COMPARTILHADAS APRESENTADOS, EM RELAÇÃO ÀS
AFIRMAÇÕES A SEGUIR, MARQUE A ALTERNATIVA
CORRETA. 
 
I. O MECANISMO MAIS COMUMENTE UTILIZADO PARA
GARANTIR A EXCLUSÃO MÚTUA É O SEMÁFORO. 
II. A ESPERA OCUPADA SE CARACTERIZA PELO LOOP
INFINITO, OCASIONANDO DESPERDÍCIOS DE RECURSOS
DO SISTEMA, NÃO EXECUTANDO NENHUM TRABALHO ÚTIL
NESSE INTERVALO, SIMPLESMENTE OCUPANDO A CPU. 
III. MESMO QUE O USO DE UM MUTEX SEJA ADOTADO,
ISSO NÃO NECESSARIAMENTE IMPÕE A SERIALIZAÇÃO DA
SEÇÃO CRÍTICA. 
A) Apenas a afirmação I está correta.
B) As afirmações I e II estão corretas.
C) As afirmações I e III estão corretas.
D) As afirmações II e III estão corretas.
E) Apenas a afirmação II está correta.
GABARITO
1. Considerando os conceitos de variáveis compartilhadas apresentados,
em relação às afirmações a seguir, marque a alternativa correta. 
 
I. O paradigma de thread estático possibilita que parte da memória que é
usada para um tipo de sistema seja preservada para outro. 
II. No paradigma de thread dinâmico, todos os threads são
bifurcados/iniciados após qualquer configuração necessária pelo thread
mestre, e os threads são executados até que todo o trabalho seja
concluído. 
III. Se os recursos necessários já estiverem disponíveis, é mais adequado
o uso do paradigma de thread estático, em vez do thread dinâmico. 
A alternativa "C " está correta.
 
No modelo de thread estático, os threads são iniciados logo após a
configuração, economizando o tempo de processamento necessário de iniciá-
los.
2. Considerando os conceitos de variáveis compartilhadas apresentados,
em relação às afirmações a seguir, marquea alternativa correta. 
 
I. O mecanismo mais comumente utilizado para garantir a exclusão
mútua é o semáforo. 
II. A espera ocupada se caracteriza pelo loop infinito, ocasionando
desperdícios de recursos do sistema, não executando nenhum trabalho
útil nesse intervalo, simplesmente ocupando a CPU. 
III. Mesmo que o uso de um mutex seja adotado, isso não
necessariamente impõe a serialização da seção crítica. 
A alternativa "E " está correta.
 
O mutex, mecanismo mais comumente utilizado para garantir a exclusão
mútua, impõe a serialização da chamada seção crítica, que se trata de um
bloco de código que só pode ser executado por um thread de cada vez.
MÓDULO 2
 Reconhecer condições de corrida e recursos utilizados para
tratamento
CONDIÇÕES DE CORRIDA
Uma condição de corrida ou risco de corrida, como vimos anteriormente, é a
condição na qual o comportamento do sistema depende da sequência ou do
tempo de outros eventos incontroláveis.
Torna-se um bug quando um ou mais dos comportamentos possíveis são
indesejáveis.
Uma condição de corrida surge no software quando um programa de
computador, para operar corretamente, depende da sequência ou do tempo
dos processos ou threads do programa.
 SAIBA MAIS
Condições críticas de corrida − que frequentemente acontecem quando os
processos ou threads dependem de algum estado compartilhado − causam
execução inválida e bugs de software. As operações em estados
compartilhados são feitas em seções críticas que devem ser mutuamente
exclusivas. O não cumprimento dessa regra pode corromper o estado
compartilhado.
Uma condição de corrida pode ser difícil de reproduzir e depurar, pois o
resultado não é determinístico e depende do tempo relativo entre threads
interferentes. Problemas dessa natureza podem, então, desaparecer durante a
execução no modo de depuração, adicionando log extra ou anexando um
depurador. Os erros que desaparecem durante as tentativas de depuração são
geralmente chamados de heisenbug. Portanto, é melhor evitar condições de
corrida por meio de um projeto de software cuidadoso.
Suponha que cada um de dois threads incremente o valor de uma variável
inteira global em 1. Idealmente, a seguinte sequência de operações ocorreria:
Thread 1 Thread 2 
Valor da
variável
 0
Ler valor ← 0
Incrementar valor 0
Escrever valor → 1
 Ler valor ← 1
 Incrementar valor 1
 Escrever valor → 2
 Atenção! Para visualização completa da tabela utilize a rolagem horizontal
 Tabela: Condição de corrida 1. 
Elaborada por: Sergio Kostin.
No caso mostrado na tabela anterior, o valor final é 2, conforme o esperado.
No entanto, se os dois threads forem executados simultaneamente, sem
bloqueio ou sincronização, o resultado da operação poderá estar errado. A
sequência alternativa de operações da tabela seguinte demonstra esse
cenário:
Thread 1 Thread 2 
Valor da
variável
 0
Ler valor ← 0
 Ler valor ← 0
Incrementar valor 0
 Incrementar valor 0
Escrever valor → 1
 Escrever valor → 1
 Atenção! Para visualização completa da tabela utilize a rolagem horizontal
 Tabela:Condição de corrida 2. 
Elaborada por: Sergio Kostin.
Nesse caso, o valor final é 1, em vez do resultado correto de 2. Isso ocorre
porque aqui as operações de incremento não são mutuamente exclusivas.
MUTUAMENTE EXCLUSIVAS
Operações mutuamente exclusivas são aquelas que não podem ser
interrompidas durante o acesso a algum recurso, como um local de
memória.
 COMENTÁRIO
Nem todos consideram as corridas de dados como um subconjunto das
condições de corrida. A definição precisa de corrida de dados é específica
para o modelo de simultaneidade formal que está sendo usado.
Normalmente, porém, refere-se a uma situação na qual uma operação de
memória em um encadeamento de threads pode tentar acessar um local de
memória ao mesmo tempo que uma operação de memória em outro
javascript:void(0)
encadeamento está escrevendo para esse local da memória, em um contexto
no qual isso é perigoso. Isso implica que uma corrida de dados é diferente de
uma condição de corrida, pois é possível haver não determinismo devido ao
tempo, mesmo em um programa sem corrida de dados. Por exemplo, em um
programa no qual todos os acessos à memória usam apenas operações
atômicas.
Isso pode ser perigoso, porque, em muitas plataformas, se dois threads
gravam em um local da memória ao mesmo tempo, é possível que o local da
memória acabe mantendo um valor que é uma combinação arbitrária e sem
sentido dos bits que representam os valores que cada thread estava tentando
escrever. Com isso, pode haver uma corrupção de memória, se o valor
resultante for um que nenhum thread tentou gravar (às vezes, isso é chamado
de "gravação interrompida").
Da mesma forma, se um thread lê um local enquanto outro thread está
gravando nele, é possível que a leitura retorne um valor que é uma
combinação arbitrária e sem sentido dos bits que representam o valor que o
local da memória mantinha antes da gravação e dos bits que representam o
valor que está sendo escrito.
 ATENÇÃO
Em muitas plataformas, operações de memória especiais são fornecidas para
acesso simultâneo; nesses casos, normalmente o acesso simultâneo é seguro,
mas o acesso simultâneo usando outras operações de memória é perigoso. Às
vezes, essas operações especiais (que são seguras para acesso simultâneo)
são chamadas de operações atômicas ou de sincronização, enquanto as
operações comuns (que não são seguras para acesso simultâneo) são
chamadas de operações de dados, sendo esse termo denominado de corrida
de dados.
SINCRONIZAÇÃO DE PROCESSOS
A sincronização é a tarefa de coordenar a execução de processos, de forma
que dois deles não possam ter acesso aos mesmos dados e recursos
compartilhados.
 ATENÇÃO
É especialmente necessária em um sistema de vários processos, quando
vários deles estão sendo executados juntos e mais de um processo tenta
obter, simultaneamente, acesso ao mesmo recurso ou dado compartilhado, o
que pode levar à inconsistência de dados.
Isso porque a mudança feita por um processo não se reflete necessariamente
quando outros acessam os mesmos dados compartilhados. Por exemplo: o
processo A altera os dados em um local da memória enquanto outro processo
B tenta ler os dados do mesmo local da memória. Há grande probabilidade de
que os dados lidos pelo segundo sejam errôneos. Portanto, para evitar esse
tipo de inconsistência de dados, os processos precisam ser
sincronizados entre si.
A sincronização de threads é definida como um mecanismo que garante que
dois ou mais processos ou threads simultâneos não executem
simultaneamente algum segmento de programa específico conhecido como
seção crítica.
O acesso dos processos à seção crítica é controlado por meio de técnicas de
sincronização. Quando um thread começa a executar a seção crítica
(segmento serializado do programa), o outro thread deve esperar até que o
primeiro termine. Se as técnicas de sincronização adequadas não forem
aplicadas, isso pode causar uma condição de corrida na qual os valores das
variáveis podem ser imprevisíveis, dependendo dos tempos de troca de
contexto dos processos ou threads.
Suponha que haja três processos: 1, 2 e 3. Todos os três estão executando
simultaneamente e precisam compartilhar um recurso comum (seção crítica),
conforme mostrado na imagem a seguir:
 
Imagem: Sergio Kostin.
 Seção crítica.
A sincronização deve ser usada aqui para evitar quaisquer conflitos para
acessar esse recurso compartilhado. Portanto, quando os Processos 1 e 2
tentam acessar esse recurso, ele deve ser atribuído a apenas um processo por
vez. Se for atribuído ao Processo 1, o Processo 2 precisa esperar até que o
primeiro libere esse recurso. Veja na próxima imagem:
 
Imagem: Sergio Kostin.
 Exclusão mútua usando seção crítica.
Precisamos satisfazer quatro condições para chegar a uma solução:
1
Dois processos nunca podem estar simultaneamente em suas regiões críticas.
2Nada pode ser afirmado sobre a velocidade ou sobre o número de CPUs.
3
Nenhum processo executando fora de sua seção crítica pode bloquear outros
processos.
4
Nenhum processo deve esperar eternamente para entrar em sua seção crítica.
Outro requisito de sincronização a ser considerado é a ordem na qual
determinados processos ou threads devem ser executados. Não é possível,
por exemplo, embarcar em um avião antes de comprar uma passagem ou
verificar e-mails antes de validar as credenciais apropriadas (nome de usuário
e senha). Da mesma forma, um caixa eletrônico não fornecerá nenhum serviço
até que receba um PIN correto.
Além da exclusão mútua, a sincronização também lida com o seguinte:
DEADLOCK (OU IMPASSE)
STARVATION (FOME)
INVERSÃO DE PRIORIDADE
ESPERA OCUPADA
DEADLOCK (OU IMPASSE)
Conforme veremos mais detalhadamente adiante, deadlocks ocorrem quando
muitos processos estão esperando por um recurso compartilhado (seção
crítica) que está sendo mantido por algum outro processo. Nesse caso, os
processos simplesmente continuam esperando e não são mais executados.
Deadlocks também podem ocorrer entre máquinas.
Por exemplo, diversos escritórios têm redes locais com vários computadores
conectados entre elas. Muitas vezes, dispositivos como scanners e
impressoras são conectadas a essas redes como recursos compartilhados,
disponíveis a qualquer usuário em qualquer máquina. Se esses dispositivos
puderem ser reservados remotamente, o mesmo tipo de deadlock poderá
ocorrer, como descrito anteriormente.
STARVATION (FOME)
Ocorre quando um processo está esperando para entrar na seção crítica, mas
outros processos a monopolizam, e o primeiro é forçado a esperar
indefinidamente.
INVERSÃO DE PRIORIDADE
Ocorre quando um processo de alta prioridade está na seção crítica e é
interrompido por um de média prioridade. Essa violação das regras de
prioridade pode acontecer em certas circunstâncias, e pode levar a sérias
consequências em sistemas de tempo real.
ESPERA OCUPADA
Ocorre quando um processo pesquisa frequentemente para determinar se tem
acesso a uma seção crítica. Essa pesquisa frequente rouba o tempo de
processamento de outros processos.
Um dos desafios para o projeto do algoritmo em ambientes distribuídos é
minimizar ou reduzir a sincronização − que pode levar mais tempo do que a
computação, especialmente na computação distribuída. A redução da
sincronização atraiu a atenção dos cientistas da computação por décadas e, à
medida que a lacuna entre a melhoria da computação e a latência aumenta,
isso se torna um problema cada vez mais significativo.
PROBLEMAS CLÁSSICOS DE
SINCRONIZAÇÃO
A seguir, alguns problemas clássicos de sincronização:
PROBLEMA PRODUTOR-CONSUMIDOR
(PROBLEMA DE BUFFER LIMITADO)
Nesse problema, há um produtor (que está produzindo algo) e um consumidor
(que está consumindo esses produtos produzidos). Os produtores e
consumidores compartilham o mesmo buffer de memória de tamanho fixo.
O trabalho do produtor é gerar os dados, colocá-los no buffer e, novamente,
começar a gerar dados. Enquanto o trabalho do consumidor é consumir os
dados do buffer.
O produtor deve produzir dados apenas quando o buffer não estiver cheio. Se
estiver cheio, o produtor não deve ter permissão para colocar nenhum dado no
buffer.
O consumidor, por sua vez, deve consumir dados apenas quando o buffer não
está vazio. Se estiver vazio, o consumidor não deve ter permissão para retirar
nenhum dado do buffer. O produtor e o consumidor não devem acessar o
buffer ao mesmo tempo.
PROBLEMA DOS LEITORES-ESCRITORES
O problema dos leitores-escritores está relacionado a um objeto, como um
arquivo que é compartilhado entre vários processos. Alguns desses processos
são leitores (querem apenas ler os dados do objeto) e alguns são escritores
(querem escrever no objeto).
O problema dos leitores-escritores é usado para gerenciar a sincronização, de
forma que não haja intercorrências com os dados do objeto.
Se dois leitores, por exemplo, acessarem o objeto ao mesmo tempo, não há
problema. No entanto, se dois escritores ou um leitor e um escritor o
acessarem ao mesmo tempo, pode haver problemas. Para resolver essa
situação, um escritor deve obter acesso exclusivo a um objeto. Ou seja,
quando um escritor está acessando o objeto, nenhum leitor ou escritor pode
acessá-lo. No entanto, vários leitores podem acessar o objeto ao mesmo
tempo.
PROBLEMA DO JANTAR DOS FILÓSOFOS
O problema do jantar dos filósofos afirma que há cinco filósofos
compartilhando uma mesa circular, e eles comem e pensam alternativamente.
Há uma tigela de arroz para cada um dos filósofos, além de cinco hashis. Um
filósofo precisa de dois hashis para comer. Um filósofo faminto só pode comer
se houver os dois talheres disponíveis. Do contrário, ele abaixa o talher e
começa a pensar novamente. O grande problema nesse algoritmo é que, se
todos os filósofos pegarem somente um hashi, todos ficarão parados para
sempre, aguardando o segundo talher ficar disponível, gerando um deadlock
(ou impasse).
Esses problemas são usados para testar quase todos os esquemas de
sincronização ou primitivos recentemente propostos.
EXCLUSÃO MÚTUA
Na ciência da computação, a exclusão mútua é uma propriedade do controle
de concorrência, instituída com o objetivo de prevenir condições de corrida.
É o requisito de que um thread de execução nunca entre em uma seção crítica
enquanto um thread simultâneo de execução já a está acessando. Refere-se a
um intervalo de tempo durante o qual um thread de execução acessa um
recurso compartilhado, como memória e objetos de dados compartilhados.
 COMENTÁRIO
Conforme vimos, a seção crítica é um objeto de dados que dois ou mais
threads simultâneos estão tentando modificar (no qual duas operações de
leitura simultâneas são permitidas, mas não são permitidas duas operações de
gravação simultâneas ou uma leitura e uma gravação, pois isso leva à
inconsistência dos dados).
O algoritmo de exclusão garante, portanto, que, se um processo já estiver
executando a operação de gravação em um objeto de dados, nenhum outro
tem permissão para acessar e modificar o mesmo objeto ou seção crítica até
que o primeiro tenha concluído a gravação e liberado o objeto para outros
processos lerem e escreverem. Em outras palavras, a seção crítica passa a
ser de uso exclusivo do processo que a acessou inicialmente.
Um exemplo simples de porque a exclusão mútua é importante na prática
pode ser visualizado usando uma lista unida de quatro itens, na qual o
segundo e o terceiro devem ser removidos, como pode ser exemplificado na
imagem a seguir:
 
Imagem: Kleber de Aguiar, adaptado por Heloise Godinho.
 Lista unida de quatro itens.
A remoção de um nó que fica entre dois outros é realizada mudando o
ponteiro próximo do nó anterior para apontar para o próximo nó. Em outras
palavras, se o nó está sendo removido, então o ponteiro próximo de nó é
alterado para apontar para o nó , removendo assim da lista encadeada
qualquer referência ao nó . Observe a imagem a seguir:
 
Imagem: Kleber de Aguiar, adaptado por Heloise Godinho.
 Lista unida após movimentação do ponteiro próximo de n-.
Quando tal lista encadeada está sendo compartilhada entre vários threads de
execução, dois threads de execução podem tentar remover dois nós diferentes
simultaneamente − um thread de execução mudando o ponteiro próximo do nó
 para apontar para o nó , enquanto outro thread de execução
muda o ponteiro próximo do nó para apontar para o nó . Embora
ambas as operações de remoção sejam concluídas com sucesso, o estado
desejado da lista vinculada não é alcançado: nó permanece na lista,
porque o ponteiro próximo do nó aponta para o nó .
Esse problema (chamado de condição de corrida) pode ser evitado usando o
requisito de exclusão mútua para garantir que atualizações simultâneas na
mesma parte da lista não ocorram.
 SAIBA MAIS
O termo exclusão mútua também é usado em referência à gravaçãosimultânea de um endereço de memória por um thread/processo, enquanto o
i  −  1 i  +  1
i i  +  2
i  +  1
i − 1 i + 1
endereço de memória mencionado anteriormente está sendo manipulado ou
lido por um ou mais threads/processos.
O problema que a exclusão mútua aborda é de compartilhamento de recursos:
Como um sistema de software pode controlar o acesso de vários processos a
um recurso compartilhado, quando cada processo precisa do controle
exclusivo desse recurso enquanto faz seu trabalho?
Para isso, a solução de exclusão mútua torna o recurso compartilhado
disponível apenas enquanto o processo está em um segmento de código
específico (seção crítica), com acesso exclusivo, impedindo que outros
processos acessem essa área de código. Ele controla o acesso ao recurso
compartilhado controlando cada execução mútua daquela parte do programa
na qual o recurso seria usado.
Uma solução bem-sucedida para esse problema deve ter pelo menos duas
propriedades:
DEVE IMPLEMENTAR A EXCLUSÃO MÚTUA
Apenas um processo pode estar na seção crítica de cada vez.
DEVE ESTAR LIVRE DE DEADLOCKS
Se os processos estão tentando entrar na seção crítica, um deles deve ser
capaz de fazê-lo com sucesso, desde que nenhum processo permaneça na
seção crítica permanentemente.
A liberdade de deadlock pode ser expandida para implementar uma ou ambas
as propriedades. A liberdade de bloqueio garante que qualquer processo que
javascript:void(0)
javascript:void(0)
deseje entrar na seção crítica será capaz de fazê-lo eventualmente. Isso é
diferente da prevenção de deadlock, que requer que algum processo em
espera seja capaz de obter acesso à seção crítica, mas não requer que cada
processo tenha sua vez. Se dois processos trocarem continuamente um
recurso entre eles, um terceiro processo pode ser bloqueado e ficar sem
recursos, mesmo que o sistema não esteja em um impasse. Se um sistema
estiver livre de bloqueios, ele garante que todos os processos possam ser
revertidos em algum ponto no futuro.
Uma propriedade de espera limitada por k fornece um compromisso mais
preciso do que a liberdade de bloqueio. Isso porque, na liberdade de bloqueio,
não dá nenhuma garantia sobre o tempo de espera. Um processo pode ser
ultrapassado um número arbitrário ou ilimitado de vezes por outros de
prioridade mais alta antes de chegar a sua vez. Sob uma propriedade de
espera limitada por k, cada processo tem um tempo de espera máximo finito.
Isso funciona estabelecendo um limite para o número de vezes que outros
processos podem interromper a fila, de modo que nenhum deles possa entrar
na seção crítica mais de k vezes enquanto outro está esperando.
O programa de cada processo pode ser dividido em quatro seções, resultando
em quatro estados. A execução do programa percorre esses quatro estados,
em ordem:
SEÇÃO NÃO CRÍTICA
A operação está fora da seção crítica; o processo não está usando ou
solicitando o recurso compartilhado.
TENTANDO
javascript:void(0)
javascript:void(0)
O processo tenta entrar na seção crítica.
SEÇÃO CRÍTICA
O processo tem permissão para acessar o recurso compartilhado nessa
seção.
SAÍDA
O processo sai da seção crítica e disponibiliza o recurso compartilhado para
outros processos.
Se um processo deseja entrar na seção crítica, ele deve primeiro executar a
seção de tentativa e esperar até obter acesso à seção crítica. Após ter
executado sua seção crítica e finalizado com os recursos compartilhados, ele
precisa executar a seção de saída para liberá-los para uso de outros
processos. O processo então retorna à sua seção não crítica.
SEÇÃO CRÍTICA
Conforme vimos, na programação simultânea, os acessos simultâneos a
recursos compartilhados podem levar a um comportamento inesperado ou
errôneo. Portanto, partes do programa onde o recurso compartilhado é
acessado precisam ser protegidas de maneiras que evitem o acesso
simultâneo. Essa seção protegida − chamada de região ou seção crítica − não
pode ser executada por mais de um processo por vez.
javascript:void(0)
javascript:void(0)
Normalmente, a seção crítica acessa um recurso compartilhado, como uma
estrutura de dados, um dispositivo periférico ou uma conexão de rede, que
não operaria corretamente no contexto de vários acessos simultâneos.
 COMENTÁRIO
Códigos ou processos diferentes podem consistir na mesma variável ou em
outros recursos que precisam ser lidos ou escritos, mas cujos resultados
dependem da ordem em que as ações ocorrem.
Por exemplo, se uma variável x deve ser lida pelo processo A e o processo B
precisa gravar na mesma variável x ao mesmo tempo, o processo A pode
obter o valor antigo ou o novo valor de x.
//Processo a 
b = x + 5 // Instrução executada em Tx 
//Processo b 
x = 3 + z // Instrução executada em Tx
Em casos como esses, uma seção crítica é importante. No exemplo anterior,
se A precisar ler o valor atualizado de x, executar o Processo A e o Processo
B ao mesmo tempo pode não dar os resultados necessários. Para evitar isso,
a variável x é protegida por uma seção crítica. Primeiramente, B obtém o
acesso à seção. Assim que B terminar de escrever o valor, A terá acesso à
seção crítica e a variável x poderá ser lida.
Controlando cuidadosamente quais variáveis são modificadas dentro e fora da
seção crítica, o acesso simultâneo à variável compartilhada é evitado.
Uma seção crítica é normalmente usada quando um programa multithreaded
deve atualizar várias variáveis relacionadas sem que um thread separado faça
alterações conflitantes nesses dados.
Em uma situação relacionada, uma seção crítica pode ser usada para garantir
que um recurso compartilhado − por exemplo, uma impressora − só possa ser
acessado por um processo por vez.
DEADLOCK
Como vimos, na computação simultânea, um deadlock é um estado em que
cada membro de um grupo espera que outro membro, incluindo ele mesmo,
execute uma ação − como enviar uma mensagem ou, mais comumente, liberar
um bloqueio.
 ATENÇÃO
Deadlock são problemas comuns em sistemas de multiprocessamento,
computação paralela e sistemas distribuídos, em que bloqueios de software e
hardware são usados para arbitrar recursos compartilhados e implementar
sincronização de processos.
Em um sistema operacional, um deadlock ocorre quando um processo ou
thread entra em um estado de espera porque um recurso de sistema solicitado
é mantido por outro processo em espera − que, por sua vez, está esperando
por outro recurso mantido por outro processo em espera.
Se um processo é incapaz de mudar seu estado indefinidamente porque os
recursos solicitados por ele estão sendo usados por outro processo em
espera, então o sistema é considerado um deadlock.
Em um sistema de comunicação, os deadlock ocorrem principalmente devido a
sinais perdidos ou corrompidos, em vez de contenção de recursos. Uma
situação de deadlock em um recurso pode surgir se e somente se todas as
seguintes condições, conhecidas como as quatro condições de Coffman et al.
1971, forem mantidas simultaneamente em um sistema:
EXCLUSÃO MÚTUA
RETER E AGUARDAR OU RETENÇÃO DE
RECURSO
SEM PREEMPÇÃO
ESPERA CIRCULAR
EXCLUSÃO MÚTUA
Como vimos anteriormente, pelo menos um recurso deve ser mantido em um
modo não compartilhável. Caso contrário, os processos não seriam impedidos
de usar o recurso quando necessário. Apenas um processo pode usar o
recurso em um determinado instante de tempo.
RETER E AGUARDAR OU RETENÇÃO DE
RECURSO
Um processo está atualmente retendo pelo menos um recurso e solicitando
recursos adicionais que estão sendo retidos por outros processos.
SEM PREEMPÇÃO
Um recurso só pode ser liberado voluntariamente pelo processo que o detém.
ESPERA CIRCULAR
Cada processo deve estar esperando por um recurso que está sendo retido
por outro processo, que, por sua vez, está aguardando que o primeiro
processo libere o recurso. Em geral, existe um conjunto de processos de
espera, P = {P1, P2, ..., PN}, tal que P1 está esperando por um recurso
mantido por P2, P2 está esperando por um recursomantido por P3 e assim
por diante, até que PN seja esperando por um recurso mantido por P1.
Embora essas condições sejam suficientes para produzir um conflito em
sistemas de recursos de instância única, elas indicam apenas a possibilidade
de conflito em sistemas com várias instâncias de recursos.
A maioria dos sistemas operacionais atuais não pode evitar bloqueios. Quando
ocorre um deadlock, diferentes sistemas operacionais respondem a eles de
maneiras diferentes e fora do padrão. A maioria das abordagens funciona
evitando que uma das quatro condições de Coffman ocorra, especialmente a
espera circular.
As principais abordagens são:
IGNORANDO IMPASSE
Nessa abordagem, presume-se que nunca ocorrerá um deadlock. É usada
quando os intervalos de tempo entre as ocorrências de deadlocks são grandes
e a perda de dados incorrida a cada vez é tolerável.
Ignorar deadlocks pode ser feito com segurança se estes forem formalmente
comprovados como nunca ocorrendo.
DETECÇÃO
Na detecção de deadlocks, presumimos que estes podem ocorrer. Em
seguida, o estado do sistema é examinado para detectar se ocorreu um
conflito e, posteriormente, corrigi-lo.
É empregado um algoritmo que rastreia a alocação de recursos e os estados
do processo, reverte e reinicia um ou mais processos para remover o impasse
detectado.
Detectar um deadlock que já ocorreu é facilmente possível, uma vez que os
recursos que cada processo bloqueou e/ou solicitou atualmente são
conhecidos pelo escalonador de recursos do sistema operacional.
Depois que um deadlock é detectado, ele pode ser corrigido usando um dos
seguintes métodos:
ENCERRAMENTO DO PROCESSO
PREEMPÇÃO DE RECURSOS
ENCERRAMENTO DO PROCESSO
Um ou mais processos envolvidos no deadlock podem ser abortados. Pode-se
optar por abortar todos os processos concorrentes envolvidos no impasse,
garantindo que o impasse seja resolvido com certeza e velocidade. Entretanto,
eventuais processamentos anteriores podem ser perdidos.
O encerramento de processos pode seguir uma sequência, até que o impasse
seja resolvido. Essa abordagem, porém, tem alto custo, porque após cada
encerramento de processo/thread, um algoritmo deve determinar se o sistema
ainda está em deadlock.
Vários fatores devem ser considerados ao escolher um candidato para o
encerramento, como prioridade e tempo de execução do processo.
PREEMPÇÃO DE RECURSOS
Os recursos alocados a vários processos podem ser sucessivamente
eliminados e alocados a outros processos até que o impasse seja resolvido.
A prevenção de deadlock funciona evitando que uma das quatro condições de
Coffman ocorra. Remover a condição de exclusão mútua significa que nenhum
processo terá acesso exclusivo a um recurso. Isso é impossível para recursos
que não podem ser armazenados em spool. Mas, mesmo com recursos em
spool, o impasse ainda pode ocorrer. Os algoritmos que evitam a exclusão
mútua são chamados de algoritmos de sincronização sem bloqueio.
javascript:void(0)
SPOOL
Processo que transfere dados colocando-os em uma área de trabalho
temporária, na qual outro programa pode acessá-lo para processá-lo em
um tempo futuro.
 COMENTÁRIO
As condições de retenção e espera ou retenção de recurso podem ser
removidas exigindo que os processos solicitem todos os recursos de que
precisarão antes de iniciar (ou antes de iniciar um determinado conjunto de
operações). Esse conhecimento prévio é frequentemente difícil de satisfazer e,
em qualquer caso, é um uso ineficiente de recursos.
Outra maneira é exigir que os processos solicitem recursos apenas quando
não houver nenhum. Primeiro, eles devem liberar todos os seus recursos
atualmente mantidos antes de solicitar todos os recursos de que precisarão do
zero. Muitas vezes, isso também é impraticável, porque os recursos podem ser
alocados e permanecer sem uso por longos períodos. Além disso, um
processo que requer um recurso popular pode ter que esperar
indefinidamente, já que este pode sempre ser alocado para algum processo,
resultando em escassez de recursos. Esses algoritmos, como tokens de
serialização, são conhecidos como algoritmos tudo ou nada.
Eles devem liberar todos os seus recursos atualmente mantidos antes de
solicitar todos os recursos de que precisarão do zero. Muitas vezes, isso
também é impraticável, porque os recursos podem ser alocados e permanecer
sem uso por longos períodos. Além disso, um processo que requer um recurso
popular pode ter que esperar indefinidamente, já que este pode sempre ser
alocado para algum processo, resultando em escassez de recursos. Esses
algoritmos, como tokens de serialização, são conhecidos como algoritmos tudo
ou nada.
A condição de ausência de preempção também pode ser difícil ou impossível
de evitar, pois um processo deve ser capaz de ter um recurso por um
determinado período de tempo, ou o resultado do processamento pode ser
inconsistente. No entanto, a incapacidade de impor a preempção pode
interferir com um algoritmo de prioridade.
A preempção de um recurso "bloqueado" geralmente implica uma reversão e
deve ser evitada, uma vez que é muito cara em despesas gerais. Os
algoritmos que permitem a preempção incluem algoritmos sem bloqueio e sem
espera e controle de simultaneidade otimista. Se um processo retém alguns
recursos e solicita algum outro que não pode ser alocado imediatamente a
eles, a condição pode ser removida liberando todos os recursos retidos
atualmente desse processo.
A condição final é a condição de espera circular. Abordagens que evitam
esperas circulares incluem desabilitar interrupções durante seções críticas e
usar uma hierarquia para determinar uma ordenação parcial de recursos. Se
nenhuma hierarquia óbvia existe, até mesmo o endereço de memória dos
recursos será usado para determinar a ordem, e os recursos serão solicitados
na ordem crescente da enumeração.
Deadlocks distribuídos podem ocorrer em sistemas distribuídos quando
transações distribuídas ou controle de simultaneidade estão sendo usados.
Eles podem ser detectados pela construção de um gráfico de espera global a
partir de gráficos de espera locais em um detector de deadlock ou por um
algoritmo distribuído como perseguição de borda.
Os sistemas distribuídos podem ter três tipos de deadlocks: fantasma, de
recurso e de comunicação. Veja-os a seguir:
DEADLOCKS FANTASMAS
DEADLOCKS DE RECURSO
DEADLOCKS DE COMUNICAÇÃO
DEADLOCKS FANTASMAS
São deadlocks falsamente detectados em um sistema distribuído devido
a atrasos internos do sistema, mas não existem de fato.
Por exemplo, se um processo libera um recurso R1 e emite uma
solicitação de R2, e a primeira mensagem é perdida ou atrasada, um
coordenador (detector de deadlocks) pode concluir falsamente um
deadlock (a solicitação de R2 enquanto tem R1 causaria um impasse).
Em uma possível situação de deadlock fantasma, é muito difícil detectá-
lo, porque vários processos podem liberar alguns recursos ou solicitar
outros recursos ao mesmo tempo.
Em alguns casos, as mensagens de liberação chegam depois das
mensagens de solicitação para alguns recursos específicos. O algoritmo
de detecção trata a situação como um deadlock, e pode abortar alguns
processos para quebrá-lo. Mas, na realidade, nenhum deadlock estava
presente − os algoritmos o detectaram com base em mensagens
desatualizadas dos processos devido a atrasos na comunicação. Esse
impasse é chamado de impasse fantasma.
DEADLOCKS DE RECURSO
Os processos podem esperar simultaneamente por vários recursos, e
não podem prosseguir até que tenham adquirido todos eles. Um conjunto
de processos fica em deadlock se cada processo no conjunto solicitar
recursos mantidos por outro processo no conjunto. E deve receber todos
os recursos solicitados antes que possa ser desbloqueado. Esse tipo de
deadlock é muito parecido com os que observamos anteriormente.
DEADLOCKS DE COMUNICAÇÃO
Os processos esperam para se comunicar com outros em um conjunto.
Um processo de espera pode ser desbloqueado ao receberuma
comunicação de um desses processos.
Há conflito se cada processo no conjunto estiver esperando para se
comunicar com outro e nenhum deles iniciar qualquer comunicação
adicional até que receba a comunicação pela qual está esperando.
DEADLOCK
Assista ao vídeo a seguir e conheça as quatro condições necessárias
para que ocorra o problema de deadlock.
VERIFICANDO O APRENDIZADO
1. A CONDIÇÃO DE CORRIDA OCORRE QUANDO DOIS OU
MAIS THREADS ACESSAM UMA VARIÁVEL
COMPARTILHADA AO MESMO TEMPO. SE NÃO FOR
TRATADA, PODE CAUSAR COMPORTAMENTOS
INDESEJADOS NA APLICAÇÃO. CONSIDERANDO OS
CONCEITOS DE EXCLUSÃO MÚTUA, DEADLOCK E
CONDIÇÕES DE CORRIDA, EM RELAÇÃO ÀS AFIRMAÇÕES
A SEGUIR, MARQUE A ALTERNATIVA CORRETA. 
 
I. UMA CONDIÇÃO DE CORRIDA − OU RISCO DE CORRIDA −
É A CONDIÇÃO EM QUE O COMPORTAMENTO DO SISTEMA
DEPENDE DA SEQUÊNCIA OU DO TEMPO DE OUTROS
EVENTOS INCONTROLÁVEIS. 
II. EXCLUSÃO MÚTUA É O REQUISITO DE QUE UM THREAD
DE EXECUÇÃO NUNCA ENTRE EM UMA SEÇÃO CRÍTICA
ENQUANTO UM THREAD SIMULTÂNEO DE EXECUÇÃO JÁ A
ESTÁ ACESSANDO. 
III. UMA SITUAÇÃO DE DEADLOCK EM UM RECURSO PODE
SURGIR SE E SOMENTE SE AS CINCO CONDIÇÕES DE
COFFMAN OCORREREM. 
A) Apenas a afirmação I está correta.
B) As afirmações I e II estão corretas.
C) As afirmações I e III estão corretas.
D) As afirmações II e III estão corretas.
E) Apenas a afirmação III está correta.
2. DEADLOCKS É UMA SITUAÇÃO INDESEJÁVEL, MAS QUE
PODE OCORRER QUANDO AS CONDIÇÕES DE COFFMAN
OCORREM EM UMA DETERMINADA APLICAÇÃO.
CONSIDERANDO OS CONCEITOS DE DEADLOCK, EM
RELAÇÃO ÀS AFIRMAÇÕES A SEGUIR, MARQUE A
ALTERNATIVA CORRETA. 
 
I. DEADLOCKS FANTASMAS SÃO DEADLOCKS QUE SÃO
FALSAMENTE DETECTADOS EM UM SISTEMA DISTRIBUÍDO
DEVIDO A ATRASOS INTERNOS DO SISTEMA, EXISTINDO
DE FATO. 
II. DEADLOCKS (OU IMPASSE) RELACIONA-SE A UMA
SITUAÇÃO EM QUE OCORRE UM IMPASSE, E DOIS OU MAIS
PROCESSOS FICAM IMPEDIDOS DE CONTINUAR SUAS
EXECUÇÕES − OU SEJA, FICAM BLOQUEADOS,
ESPERANDO UNS PELOS OUTROS. 
III. UMA FORMA DE DETECTAR DEADLOCKS DISTRIBUÍDOS
É PELA CONSTRUÇÃO DE UM GRÁFICO DE ESPERA
GLOBAL, CONSTRUÍDO A PARTIR DE GRÁFICOS DE
ESPERA LOCAIS. 
A) Apenas a afirmação I está correta.
B) As afirmações I e II estão corretas.
C) As afirmações I e III estão corretas.
D) As afirmações II e III estão corretas.
E) Apenas a afirmação III está correta.
GABARITO
1. A condição de corrida ocorre quando dois ou mais threads acessam
uma variável compartilhada ao mesmo tempo. Se não for tratada, pode
causar comportamentos indesejados na aplicação. Considerando os
conceitos de exclusão mútua, deadlock e condições de corrida, em
relação às afirmações a seguir, marque a alternativa correta. 
 
I. Uma condição de corrida − ou risco de corrida − é a condição em que o
comportamento do sistema depende da sequência ou do tempo de
outros eventos incontroláveis. 
II. Exclusão mútua é o requisito de que um thread de execução nunca
entre em uma seção crítica enquanto um thread simultâneo de execução
já a está acessando. 
III. Uma situação de deadlock em um recurso pode surgir se e somente
se as cinco condições de Coffman ocorrerem. 
A alternativa "B " está correta.
 
Um deadlock ocorre se e somente se as quatro condições de Coffman forem
satisfeitas:
Exclusão mútua.
Posse e espera.
Não preempção.
Espera circular.
2. Deadlocks é uma situação indesejável, mas que pode ocorrer quando
as condições de Coffman ocorrem em uma determinada aplicação.
Considerando os conceitos de deadlock, em relação às afirmações a
seguir, marque a alternativa correta. 
 
I. Deadlocks fantasmas são deadlocks que são falsamente detectados em
um sistema distribuído devido a atrasos internos do sistema, existindo
de fato. 
II. Deadlocks (ou impasse) relaciona-se a uma situação em que ocorre um
impasse, e dois ou mais processos ficam impedidos de continuar suas
execuções − ou seja, ficam bloqueados, esperando uns pelos outros. 
III. Uma forma de detectar deadlocks distribuídos é pela construção de
um gráfico de espera global, construído a partir de gráficos de espera
locais. 
A alternativa "D " está correta.
 
Três tipos de deadlocks podem ocorrer em um sistema distribuído:
comunicações, recursos e fantasma. Os deadlocks fantasmas não existem de
fato, eles são falsamente detectados por problemas de atrasos internos em um
sistema distribuído. O atraso no envio das mensagens faz com que o algoritmo
de detecção suponha a existência do deadlock, sem que ele efetivamente
tenha ocorrido.
MÓDULO 3
 Distinguir as diferentes abstrações de programação, como semáforos,
monitores e suas aplicações
SEMÁFOROS
Na ciência da computação, um semáforo é um tipo de dado variável ou
abstrato empregado para controlar o acesso a um recurso comum por vários
processos e evitar problemas de seção crítica em um sistema simultâneo,
como um sistema operacional multitarefa. Um semáforo trivial é uma variável
simples que é alterada (por exemplo, incrementada ou decrementada ou
alternada), dependendo das condições definidas pelo programador.
 ATENÇÃO
Os semáforos são uma ferramenta útil na prevenção de condições de corrida;
entretanto, seu uso não é de forma alguma uma garantia de que um programa
esteja livre desses problemas.
Os semáforos que permitem uma contagem arbitrária de recursos são
chamados de semáforos de contagem, enquanto os que são restritos aos
valores 0 e 1 (ou bloqueado/desbloqueado, indisponível/disponível) são
chamados de semáforos binários e são usados para implementar bloqueios.
 EXEMPLO
Suponha que uma biblioteca tenha dez salas de estudo idênticas, para serem
usadas por um aluno por vez. Os alunos devem solicitar na recepção, caso
desejem usar uma sala de estudo e, se nenhuma estiver disponível, eles
esperam na mesa. Quando um aluno terminar de usar uma sala, ele deve
retornar à carteira e indicar que uma das salas ficou livre.
Na implementação mais simples, o funcionário da recepção sabe apenas o
número de salas livres disponíveis. Quando um aluno solicita uma sala, o
funcionário diminui esse número. Quando um aluno libera uma sala, o
secretário aumenta esse número. As salas podem ser usadas por tempo
indeterminado, por isso não é possível reservá-las com antecedência.
Nesse cenário, o contador da recepção representa um semáforo de contagem,
as salas são o recurso e os alunos representam processos/threads. O valor do
semáforo é inicialmente 10, com todas as salas vazias. Quando um aluno
solicita uma sala, o valor do semáforo é alterado para 9. Depois que o próximo
aluno chega, ele cai para 8, depois para 7 e assim por diante. Se alguém
solicita uma sala e o valor atual do semáforo é 0, eles são forçados a esperar
até que uma delas esteja disponível (quando a contagem é aumentada de 0).
Se uma das salas foi liberada, mas há vários alunos esperando, então
qualquer método pode ser usado para selecionar aquele que irá ocupá-la. E os
alunos precisam informar ao balconista sobre a liberação de sua sala somente
depois de realmente deixá-la; caso contrário, pode haver uma situação
embaraçosa quando este está em processo de deixar a sala (está
empacotando seus livros etc.) e outro aluno entra antes de ele sair.
Os semáforos são muito úteis na sincronização de processos e multithreading.
Temos a biblioteca de semáforo POSIX em sistemas Linux, e vamos aprender
a usá-lo.
Salientamos que semáforos existem nas mais diversas linguagens que
permitem programação paralela, bem como em diferentes sistemas
operacionais.
O código básico de um semáforo é simples, conforme apresentado em nosso
estudo. Mas esse código não pode ser escrito diretamente, pois as funções
precisam ser atômicas, e escrever o código diretamente levaria a uma troca de
contexto sem a conclusão da função e resultaria em uma bagunça.
Para bloquear um semáforo ou esperar, podemos usar a função sem_wait:
int sem_wait(sem_t *sem);
Para liberar ou sinalizar um semáforo, usamos a função sem_post:
int sem_post(sem_t *sem);
Um semáforo é inicializado usando sem_init(para processos ou threads) ou
sem_open (para IPC).
sem_init(sem_t *sem, int pshared, unsigned int value);
SEÇÃO CRÍTICA: TÉCNICAS DE
SINCRONIZAÇÃO
Como vimos no módulo anterior, o acesso dos processos à seção
crítica (Segmento serializado do programa.) é controlado pelo uso de
técnicas de sincronização. Quando um thread começa a executar a seção
crítica, o outro thread deve esperar até que o primeiro thread termine.
Se as técnicas de sincronização adequadas não forem aplicadas, isso pode
causar uma condição de corrida em que os valores das variáveis podem ser
imprevisíveis e variam dependendo dos tempos de alternância de contexto dos
processos ou threads.
Vejamos um exemplo de código para estudar problemas de sincronização por
meio de uma seção crítica:
#include <pthread.h> 
#include <stdio.h> 
#include <stdlib.h> 
#include <string.h> 
#include <unistd.h> 
pthread_t tid[2]; 
int counter; 
pthread_mutex_t lock; 
void * trythis(void* arg) 
{ 
 pthread_mutex_lock(&lock); 
 unsigned long i = 0; 
 counter += 1;
 printf("\n Job %d has started\n", counter); 
 for (i = 0; i < (0xFFFFFFFF); i++); 
 printf("\n Job %d has finished\n", counter); 
 pthread_mutex_unlock(&lock); 
 return NULL; 
} 
 
int main(void) 
{ 
 int i = 0; 
 int error; 
 
 if (pthread_mutex_init(&lock, NULL) != 0) { 
 printf("\n mutex init has failed\n"); 
 return 1; 
 } 
 
 while (i < 2) { 
 error = pthread_create(&(tid[i]), NULL, &trythis, NULL); 
 if (error != 0) 
 printf("\nThread can't be created :[%s]", 
 strerror(error)); 
 i++; 
 } 
 pthread_join(tid[0], NULL); 
 pthread_join(tid[1], NULL); 
 pthread_mutex_destroy(&lock); 
 return 0; 
}
No código anterior, percebemos que:
Um mutex é inicializado no início da função principal.
O mesmo mutex está bloqueado na função ‘trythis ()’ ao usar o recurso
compartilhado ‘contador’.
No final da função ‘trythis ()’, o mesmo mutex é desbloqueado.
No final da função principal, quando ambas os threads estão concluídos, o
mutex é destruído.
Resultado:
Job 1 started 
Job 1 finished 
Job 2 started 
Job 2 finished
MONITOR
Para superar os erros de tempo que ocorrem ao usar o semáforo para
sincronização de processos, foi introduzida uma construção de sincronização
de alto nível denominada monitor. Um tipo de monitor é um tipo de dados
abstrato usado para sincronização de processos.
Sendo um tipo de dados abstrato, o tipo de monitor contém as variáveis de
dados que devem ser compartilhados por todos os processos e algumas
operações definidas pelo programador, que permitem que os processos sejam
executados em exclusão mútua dentro do monitor.
Um processo não pode acessar diretamente a variável de dados
compartilhados no monitor. Ele deve acessá-lo por meio de procedimentos
definidos no monitor que permitem que apenas um processo acesse as
variáveis compartilhadas em um monitor por vez. Ou seja, um monitor é uma
construção em que apenas um processo está ativo por vez. Se outro processo
tentar acessar a variável compartilhada no monitor, ele será bloqueado e será
alinhado na fila para obter o acesso.
A sintaxe do monitor é a seguinte:
monitor monitor_name 
{ 
 //shared variable declarations 
 procedure P1 ( . . . ) { 
 } 
 procedure P2 ( . . . ) { 
 } 
 procedure Pn ( . . . ) { 
 } 
 initialization code ( . . . ) { 
 } 
}
Nos monitores, há a introdução do conceito de variáveis condicionais como um
mecanismo de sincronização adicional. A variável condicional permite que um
processo aguarde dentro do monitor e que um processo de espera seja
retomado imediatamente quando o outro libera os recursos.
 ATENÇÃO
A variável condicional pode invocar apenas duas operações, wait () e signal ().
Se um processo P invocar uma operação wait (), ele é suspenso no monitor
até que outro processo Q invoque a operação signal (); ou seja, uma operação
signal () invocada por um processo retoma o processo suspenso.
DIFERENÇA ENTRE SEMÁFORO E
MONITOR
Veja no quadro abaixo algumas diferenças entre semáforo e monitor:
Semáforos Monitores
O semáforo é uma variável inteira
S que indica o número de
recursos disponíveis no sistema
O monitor é o tipo de dado
abstrato que permite que apenas
um processo seja executado na
seção crítica por vez.
O valor do semáforo pode ser Um monitor, por sua vez, possui
modificado apenas pelas
operações e .
as variáveis compartilhadas e os
procedimentos apenas por meio
dos quais as variáveis
compartilhadas podem ser
acessadas pelos processos.
No semáforo, quando um
processo deseja acessar recursos
compartilhados, o processo
executa a operação e
bloqueia os recursos e, quando
libera os recursos, executa a
operação .
Em monitores, quando um
processo precisa acessar
recursos compartilhados, ele deve
acessá-los por meio de
procedimentos no monitor.
 Atenção! Para visualização completa da tabela utilize a rolagem horizontal
 Tabela: Diferenças entre semáforos e monitores. 
Elaborada por: Sergio Kostin, adaptado por Ramany Oliveira.
Além disso, o tipo de monitor possui variáveis de condição que o semáforo não
possui.
wait() shmget ()
wait()
signal()
SEMÁFOROS E MONITORES
Assista ao vídeo a seguir e acompanhe algumas explicações sobre semáforos
e monitores com exemplos do emprego dos recursos. Você baixar o código
utilizado no vídeo clicando aqui.
https://estacio.webaula.com.br/cursos/temas/01606/docs/semaforo.c
VERIFICANDO O APRENDIZADO
1. EM RELAÇÃO ÀS AFIRMAÇÕES A SEGUIR, MARQUE A
ALTERNATIVA CORRETA. 
 
I. UM SEMÁFORO É UM TIPO DE DADO VARIÁVEL OU
ABSTRATO USADO PARA CONTROLAR O ACESSO A UM
RECURSO COMUM POR VÁRIOS PROCESSOS E EVITAR
PROBLEMAS DE SEÇÃO CRÍTICA EM UM SISTEMA
SIMULTÂNEO, COMO UM SISTEMA OPERACIONAL
MULTITAREFA. 
II. UM SEMÁFORO TRIVIAL SE ASSEMELHA A UMA
VARIÁVEL SIMPLES, QUE É ALTERADA (INCREMENTADA,
DECREMENTADA OU ALTERNADA) DE ACORDO COM A
OPERAÇÃO DEFINIDA PELO PROGRAMADOR. 
III. OS SEMÁFOROS SÃO FERRAMENTAS UTILIZADAS NA
PREVENÇÃO DE CONDIÇÕES DE CORRIDA, E SEU USO
GARANTE QUE UM PROGRAMA ESTEJA LIVRE DESSES
PROBLEMAS. 
A) Apenas a afirmação I está correta.
B) As afirmações I e II estão corretas.
C) As afirmações I e III estão corretas.
D) As afirmações II e III estão corretas.
E) Apenas a afirmação III está correta.
2. EM RELAÇÃO ÀS AFIRMAÇÕES A SEGUIR, MARQUE A
ALTERNATIVA CORRETA. 
 
I. UMA DAS DIFERENÇAS ENTRE O TIPO DE SEMÁFORO E
O TIPO DE MONITOR É QUE O PRIMEIRO POSSUI
VARIÁVEIS DE CONDIÇÃO, JÁ O SEGUNDO NÃO AS
POSSUI. 
II. A OPERAÇÃO SIGNAL() É A ÚNICA MANEIRA PELA QUAL
O VALOR DO SEMÁFORO PODE SER MODIFICADO. 
III. O SEMÁFORO É UMA VARIÁVEL INTEIRA S QUE INDICA
A QUANTIDADE DE RECURSOS DISPONÍVEIS NO SISTEMA,
ENQUANTO O MONITOR É O TIPO DE DADO ABSTRATO
QUE PERMITE QUE APENAS UM PROCESSO SEJA
EXECUTADO NA SEÇÃO CRÍTICA POR VEZ.
A) Apenas a afirmação I está correta.
B) As afirmações I e II estão corretas.
C) As afirmações I e III estão corretas.
D) As afirmações II e III estão corretas.
E) Apenas a afirmação III está correta.
GABARITO
1. Em relação às afirmações a seguir, marque a alternativa correta. 
 
I. Um semáforo é um tipo de dado variável ou abstrato usado para
controlar o acesso a um recurso comum por vários processos e evitar
problemas de seção crítica em um sistema simultâneo, como um sistema
operacional multitarefa. 
II. Um semáforo trivial se assemelha a uma variável simples, que é
alterada (incrementada, decrementada ou alternada) de acordo com a
operação definida pelo programador. 
III. Os semáforos são ferramentas utilizadas na prevenção de condições
de corrida, e seu uso garante que um programa esteja livre desses
problemas. 
A alternativa "B " está correta.
 
Como vimos, existem diversos fatores que podem causar condições de
corrida. O simples uso do semáforo não é garantia da prevenção das
condições de corrida.
2. Em relação às afirmações a seguir, marque a alternativa correta. 
 
I. Uma das diferenças entre o tipo de semáforo e o tipo de monitoré que
o primeiro possui variáveis de condição, já o segundo não as possui. 
II. A operação signal() é a única maneira pela qual o valor do semáforo
pode ser modificado. 
III. O semáforo é uma variável inteira S que indica a quantidade de
recursos disponíveis no sistema, enquanto o monitor é o tipo de dado
abstrato que permite que apenas um processo seja executado na seção
crítica por vez.
A alternativa "E " está correta.
 
Variáveis de condição são uma exclusividade do tipo de monitor, um semáforo
não as possui.
O valor do semáforo pode ser modificado tanto pela operação signal(), quanto
pela operação wait().
MÓDULO 4
 Empregar diferentes ambientes de programação, em especial
corrotinas, Pthreads, OpenMP e Erlang
CORROTINAS
Corrotinas são componentes de programas de computador que generalizam
sub-rotinas para multitarefa não preemptiva, permitindo que a execução seja
suspensa e reiniciada.
Os sistemas Windows iniciais não eram preemptivos, e o uso de corrotinas
permitia que se alternassem dentro dos processos em execução.
As sub-rotinas são casos especiais de corrotinas. Quando são chamadas, a
execução começa no início e, quando uma sub-rotina for encerrada, ela é
concluída. Uma instância de uma sub-rotina só retorna uma vez, e não
mantém o estado entre as invocações.
As corrotinas, por sua vez, podem sair chamando outras corrotinas, que
podem posteriormente retornar ao ponto onde foram chamadas na corrotina
original. Do seu ponto de vista, ela não está saindo, mas chamando outra
corrotina. Assim, uma instância de corrotina mantém o estado e varia entre as
invocações − pode haver várias instâncias de uma determinada corrotina de
uma vez.
 COMENTÁRIO
A diferença entre chamar outra corrotina por meio de "ceder" a ela e
simplesmente chamar outra rotina (que então, também, voltaria ao ponto
original), é que a relação entre duas corrotinas que cedem uma à outra não é
a do chamador, mas simétrica.
Vejamos um exemplo simples de como as corrotinas podem ser úteis:
 EXEMPLO
Suponha que você tenha um relacionamento consumidor-produtor em que
uma rotina cria itens e os adiciona a uma fila e outra remove itens da fila e os
usa. Por razões de eficiência, você deseja adicionar e remover vários itens de
uma vez. O código pode ser assim:
var q := new queu 
coroutine produce 
 loop 
 while q is not full 
 create some new items 
 add the items to q 
 yield to consume 
coroutine consume 
 loop 
 while q is not empty 
 remove some items from q 
 use the items 
 yield to produce 
call produce
A fila é completamente preenchida ou esvaziada antes de ceder o controle
para a outra corrotina usando o comando yield. As chamadas de corrotinas
adicionais começam logo após o rendimento, no loop de corrotinas externo.
Embora esse exemplo seja frequentemente usado como uma introdução ao
multithreading, dois threads não são necessários para isso: a instrução yield
pode ser implementada por um salto direto de uma rotina para a outra.
As vantagens das corrotinas sobre os threads são:
Elas podem ser usadas em um contexto de tempo real rígido (alternar entre as
corrotinas não precisa envolver nenhuma chamada do sistema ou qualquer
chamada de bloqueio).
Não há necessidade de primitivas de sincronização, como mutexes, semáforos
etc. para proteger as seções críticas.
Não há necessidade de suporte do sistema operacional.
É possível implementar corrotinas usando encadeamentos de tarefas
agendados preventivamente, de forma que seja transparente para o código de
chamada, mas algumas das vantagens (particularmente a adequação para
operação em tempo real e relativa economia de alternar entre eles) serão
perdidas.
As corrotinas são úteis para implementar o seguinte:
MÁQUINAS DE ESTADO
Dentro de uma única sub-rotina, em que o estado é determinado pelo ponto de
entrada e de saída atual do procedimento. Isso pode resultar em um código
mais legível em comparação com o uso da diretiva goto, e pode ser
implementado por meio de recursão mútua com chamadas finais.
MODELO DE ATOR DE SIMULTANEIDADE
Por exemplo, em videogames, cada ator tem seus próprios procedimentos
(isso, novamente, separa logicamente o código), mas eles cedem de forma
voluntária o controle ao escalonador central, que os executa sequencialmente
(essa é uma forma de multitarefa cooperativa).
COMUNICAR PROCESSOS SEQUENCIAIS
Ocorre quando cada subprocesso é uma corrotina. As entradas/saídas do
canal e as operações de bloqueio geram corrotinas, e um planejador as
desbloqueia em eventos de conclusão. Alternativamente, cada subprocesso
pode ser o pai daquele que o segue no pipeline de dados (ou o precede, caso
em que o padrão pode ser expresso como geradores aninhados).
COMUNICAÇÃO REVERSA
Comumente usada em software matemático, no qual um procedimento como
um solucionador, avaliador integral, precisa do processo de uso para fazer um
cálculo, como avaliar uma equação ou integrando.
PTHREADS
Um dos conceitos mais importantes relacionados a sistemas operacionais é
denominado processo. De modo geral, um processo é uma abstração de um
programa em execução.
Tal programa possui um espaço de endereçamento e, em sistemas
tradicionais, apenas um thread (segmento ou fluxo de controle) de execução.
Além disso, o processo contém toda informação relacionada ao seu contexto
de execução, por exemplo, contador de programa, apontador de pilha e
demais registradores. Ou seja, é um programa com sua função principal,
denominada , sendo executado sequencialmente, instrução por
instrução.
No entanto, em muitos sistemas operacionais é possível criar mais de um
thread no mesmo processo, isto é, no mesmo espaço de endereçamento.
Nesse caso, mais de um fluxo de execução ocorre dentro do mesmo processo.
Diante disso, é importante destacar algumas aplicações:
Tratar atividades que ocorrem “simultaneamente”.
main
Dividir a aplicação em tarefas que acessam recursos compartilhados.
Reduzir o tamanho de uma aplicação, uma vez que threads ocupam menos
espaço em relação aos processos.
São mais fáceis de criar e destruir.
A sobreposição de tarefas pode acelerar a aplicação.
Possibilitam paralelismo real em sistemas multicore.
É importante destacar que threads e processos são conceitos diferentes.
Processo
Como dito anteriormente, o processo é basicamente um agrupador de
recursos (código e dados) e possui uma identidade.

Threads
Os threads são criados no contexto de um processo e compartilham o mesmo
espaço de endereçamento.
Em vista disso, threads não são independentes como os processos, pois,
embora compartilhem o mesmo espaço de endereçamento dentro de um
processo, cada thread possui os mecanismos para gerenciar seu contexto de
execução. Assim, threads possuem seu próprio contador de programa, seu
apontador de pilha e seus registradores.
Os threads criados ocupam a CPU do mesmo modo que o processo criador, e
são escalonados pelo próprio processo. Nesse contexto, quando uma
aplicação multithread é executada, esses threads podem estar em qualquer
um destes estados: em execução, bloqueado (aguardando), pronto para ser
executado ou concluído (finalizado), conforme ilustrado na imagem a seguir:
 
Imagem: Sergio Kostin.
 Estados de um thread.
Para padronizar a utilização de threads em diversos sistemas, o
IEEE (Institute of Electrical and Electronics Engineers) estabeleceu o padrão
POSIX threads (IEEE 1003.1c), ou Pthreads. Esse padrão define mais de 60
funções − definidas na biblioteca pthreads.h − para criar e gerenciar threads.
Além disso, a biblioteca define estruturas de dados e atributos para configurar
os threads. De modo geral, esses atributos são passados como argumentos
para os parâmetros das funções, por exemplo:
 EXEMPLO
pthread_t: Handle para pthread, isto é, um valor que permite identificar o
thread.
pthread_attr_t: Atributos para configuração de thread.
Esses recursos são utilizados nas principais funções − apresentadas a seguir
−para criação e gerenciamento de threads. Cabe ressaltar que as funções
que retornam valor têm como padrão o inteiro 0, indicando sucesso. Assim,
qualquer inteiro diferente de zero é um código de erro.
OPENMP
OpenMP é uma interface de programação de aplicativo (API) de memória
compartilhada cujos recursos, como acabamos de ver, são baseados em
esforços anteriores para facilitar a programação paralela de memória
compartilhada.
O OpenMP se destina à implementação em uma ampla gama de arquiteturas
SMP.
 ATENÇÃO
À medida que máquinas multicore e processadores multithreading se
espalham no mercado, eles podem ser cada vez mais usados para criar
programas para computadores uniprocessadores.
Como seus predecessores, OpenMP não é uma nova linguagem de
programação. Em vez disso, é uma notação que pode ser adicionada a um
programa sequencial em C ou C++ para descrever como o trabalho deve ser
compartilhado entre threads que serão executados em diferentes
processadores ou núcleos e para solicitar acessos a dados compartilhados
conforme necessário.
A inserção apropriada de recursos OpenMP em um programa sequencial
permitirá que muitos, talvez a maioria, dos aplicativos se beneficiem de
arquiteturas paralelas de memória compartilhada − geralmente com
modificações mínimas no código. Na prática, muitos aplicativos têm um
paralelismo considerável que pode ser explorado.
O sucesso do OpenMP pode ser atribuído a vários fatores:
Sua forte ênfase na programação paralela estruturada.
O fato de ser comparativamente simples de usar, uma vez que a
responsabilidade de trabalhar os detalhes do programa paralelo é do
compilador.
A grande vantagem de ser amplamente adotado, de modo que um aplicativo
OpenMP pode ser executado em muitas plataformas diferentes.
 COMENTÁRIO
Mas, acima de tudo, o OpenMP é oportuno. Com o forte crescimento na
implantação de SMPs pequenos e grandes e outros hardwares multithreading,
a necessidade de um padrão de programação de memória compartilhada que
seja fácil de aprender e aplicar é aceita em todo o setor. Os fornecedores por
trás do OpenMP entregam coletivamente grande fração dos SMPs em uso nos
dias atuais. Seu envolvimento com esse padrão de fato garante sua
aplicabilidade contínua às suas arquiteturas.
Confira agora seus prós e contras:
PRÓS
Código multithreading portátil (em C/C++ e outras linguagens,
normalmente é necessário chamar primitivas específicas da plataforma
para obter multithreading).
Não precisa lidar com a passagem de mensagens como o MPI faz.
O layout e a decomposição dos dados são controlados automaticamente
por diretivas.
Escalabilidade comparável ao MPI em sistemas de memória
compartilhada.
Paralelismo incremental: pode funcionar em uma parte do programa ao
mesmo tempo, sendo que nenhuma mudança drástica no código é
necessária.
Código unificado para aplicativos seriais e paralelos: construções
OpenMP são tratadas como comentários quando compiladores
sequenciais são usados.
As instruções de código original (serial) não precisam, em geral, ser
modificadas quando paralelizadas com OpenMP. Isso reduz a chance de
introduzir bugs inadvertidamente.
Tanto o paralelismo de granulação grossa quanto o de granulação fina
são possíveis.
Em aplicações multifísicas irregulares que não aderem apenas ao modo
de computação SPMD (Single Program Multiple Data), conforme
encontrado em sistemas de partículas de fluido fortemente acoplados, a
flexibilidade do OpenMP pode ter grande vantagem de desempenho sobre
o MPI.
Pode ser usado em vários aceleradores, como GPGPU (General Purpose
Graphics Processing Unit) e FPGAs (Field Programmable Gate Array).
CONTRAS
Risco de introdução de bugs de sincronização difíceis de depurar e
condições de corrida.
A partir de 2017, funciona de forma eficiente somente em plataformas de
multiprocessador de memória compartilhada.
Requer um compilador que suporte OpenMP.
A escalabilidade é limitada pela arquitetura da memória.
Sem suporte para comparar e trocar.
Falta um tratamento confiável de erros.
Carece de mecanismos refinados para controlar o mapeamento do
processador de thread.
Alta chance de escrever código de compartilhamento falso
acidentalmente.
ERLANG
Erlang é uma linguagem declarativa para programar sistemas concorrentes e
distribuídos que foi desenvolvida no Ericsson e Ellemtel Computer Science
Laboratories.
O desenvolvimento de Erlang começou como uma investigação sobre se os
paradigmas modernos de programação declarativa poderiam ser usados para
programar grandes sistemas de computação de telecomunicações industriais.
Logo se percebeu que as linguagens adequadas para a programação de
sistemas de telecomunicações também eram adequadas para uma ampla
gama de problemas de controle em tempo real embutidos na indústria.
Muitas das primitivas Erlang fornecem soluções para problemas que são
comumente encontrados durante a programação de grandes sistemas
simultâneos de tempo real:
O sistema de módulos permite a estruturação de programas muito grandes em
unidades conceitualmente gerenciáveis.
Os mecanismos de detecção de erros possibilitam a construção de software
tolerante a falhas.
As primitivas de carregamento de código permitem que o código em um
sistema em execução seja alterado sem parar o sistema.
Erlang tem um modelo de concorrência baseado em processo. A
simultaneidade é explícita, e o usuário pode controlar com precisão quais
cálculos são executados sequencialmente e quais são executados em
paralelo.
A passagem da mensagem entre os processos é assíncrona, ou seja, o
processo de envio continua assim que a mensagem é enviada.
O único método pelo qual os processos Erlang podem trocar dados é a
passagem de mensagens. Isso resulta em aplicativos que podem ser
facilmente distribuídos − um aplicativo escrito para um uniprocessador pode
ser alterado para rodar em um multiprocessador ou rede de uniprocessadores.
A linguagem possui mecanismos embutidos para programação distribuída, que
torna mais fácil escrever aplicativos que podem ser executados em um único
computador ou em uma rede de computadores.
Variáveis em Erlang têm a propriedade de atribuição única − uma vez que um
valor foi atribuído a uma variável, esse valor nunca pode ser alterado. Essa
propriedade tem consequências importantes ao depurar ou transformar um
programa, que é escrito inteiramente em termos de funções. A seleção de
função é feita por correspondência de padrões que leva a programas
altamente sucintos.
O sistema Erlang tem uma noção embutida de tempo, ou seja, o programador
pode especificar quanto tempo um processo deve esperar por uma mensagem
antes de realizar alguma ação. Isso permite a programação de aplicativos em
tempo real, tornando o Erlang adequado para a maioria das aplicações soft em
tempo real nas quais os tempos de resposta são da ordem de milissegundos.
 COMENTÁRIO
As técnicas de programação para sistemas simultâneos de tempo real ficaram,
por muitos anos, atrás das técnicas usadas para programar aplicativos
sequenciais. Quando o uso de linguagens como C ou Pascal era uma prática
padrão para a programação de aplicativos sequenciais, a maioria dos
programadores de sistemas de tempo real ainda lutava com as linguagens
assembly. Os sistemas de tempo real atuais podem ser escritos em linguagens
como Ada, Modula2, Occam etc., nas quais existem construções explícitas
para simultaneidade de programação, ou em linguagens como C, que não
possuem construções para simultaneidade.
O interesse em simultaneidade é motivado por um estudo de problemas que
exibem alto grau de simultaneidade natural − essa é uma propriedade típica de
problemas de controle em tempo real. O programador Erlang determina
explicitamente quais atividades devem ser representadas como processos
paralelos.
 ATENÇÃO
Essa visão de simultaneidade é semelhante à encontrada em Occam, CSP,
Concurrent Pascal etc., mas diferente das linguagens concorrentes. Nestas, a

Outros materiais