Você está na página 1de 113

Programação Concorrente e

Distribuída
Aula 1 – conceitos básicos
Remis Balaniuk, PhD
Plano de ensino
• Nota Teórica (NT): (0 a 10) correspondendo às média das 2 provas realizadas ao
longo da disciplina.

• Nota Prática (NP): 0 a 10, correspondendo à média dos exercícios realizados no


laboratório (peso 1 cada) e do projeto final (peso 2).
• Obs.: serão atribuídas notas individuais aos alunos mesmo para atividades efetuadas em
grupo. Trabalhos copiados de qualquer fonte terão nota igual a zero.

• Extras (EX) :0 a 1, correspondendo aos exercícios propostos durante as aulas

• Média Final: (NT+NP)/2 + EX;

• Recuperação: haverá ao final do semestre uma Atividade Avaliativa de


recuperação/substituição (AR) que será utilizada para substituir todas as notas
Ferramentas
• AVA:
• conteúdo, msgs, tarefas, exercícios
• Teams:
• Aulas síncronas
• Gravação das aulas
• Material de apoio
• Programação:
• Java
• Exercícios em sala de aula e extraclasse
• Tenha uma IDE à mão (Eclipse)
Como acelerar o processamento?

• “Se um único computador (processador) consegue


resolver um problema em N segundos, podem N
computadores (processadores)resolver o mesmo
problema em 1 segundo?”
Porque programação paralela?
• Dois dos principais motivos para utilizar programação paralela são:
• Reduzir o tempo necessário para solucionar um problema.
• Resolver problemas mais complexos e de maior dimensão.
• Outros motivos são:
• Tirar partido de recursos computacionais não disponíveis localmente ou
subaproveitados.
• Ultrapassar limitações de memória quando a memória disponível num único
computador é insuficiente para a resolução do problema.
• Ultrapassar os limites físicos de velocidade e de miniaturização que
atualmente começam a restringir a possibilidade de construção de
computadores sequenciais cada vez mais rápidos.
Porque programação paralela?
• Tradicionalmente, a programação paralela foi motivada pela resolução/simulação
de problemas fundamentais da ciência/engenharia de grande relevância científica
e econômica, denominados como Grand Challenge Problems (GCPs).
• Tipicamente, os GCPs simulam fenômenos que não podem ser medidos por
experimentação:
• Fenômenos climáticos (e.g. movimento das placas tectónicas)
• Fenômenos físicos (e.g. órbita dos planetas)
• Fenômenos químicos (e.g. reações nucleares)
• Fenômenos biológicos (e.g. genoma humano)
• Fenômenos geológicos (e.g. actividade sísmica)
• Componentes mecânicos (e.g. aerodinâmica/resistência de materiais em naves espaciais)
• Circuitos eletrônicos (e.g. verificação de placas de computador)...
Porque programação paralela?
• Atualmente, as aplicações que exigem o desenvolvimento de
computadores cada vez mais rápidos estão por todo o lado.
• Estas aplicações ou requerem um grande poder de computação ou
requerem o processamento de grandes quantidades de informação.
• Alguns exemplos são:
• Bases de dados paralelas
• Mineração de dados (data mining)
• Serviços de procura baseados na webServiços associados a tecnologias
multimédia e telecomunicações
• Computação gráfica e realidade virtual
• Diagnóstico médico assistido por computador
• Gestão de grandes industrias/corporações
• ...
Paradigmas da computação
• Sequencial x Concorrente (Paralela)
Programação sequencial
• Um execução é considerada sequencial quando este é visto como
uma série de instruções sequenciais que devem ser executadas em
um único processador.
Programação concorrente (paralela)
• Um programa é considerado programação paralela quando este é
visto como um conjunto de partes que podem ser resolvidas
concorrentemente.
• Cada parte é igualmente constituída por uma série de instruções
sequenciais, mas que no seu conjunto podem ser executadas
simultaneamente em vários processadores.
Concorrência ou Paralelismo Potencial
• Concorrência ou paralelismo potencial diz-se quando um programa
possui tarefas (partes contíguas do programa) que podem ser
executadas em qualquer ordem sem alterar o resultado final.
Paralelismo
• Paralelismo diz-se quando as tarefas de um programa são executadas
em simultâneo em mais do que um processador.
Paralelismo Implícito
• O paralelismo diz-se implícito quando cabe ao compilador e ao
sistema de execução:
• Detectar o paralelismo potencial do programa.
• Atribuir as tarefas para execução em paralelo.
• Controlar e sincronizar toda a execução.
• Vantagens e inconvenientes:
• (+) Liberta o programador dos detalhes da execução paralela.
• (+) Solução mais geral e mais flexível.
• (–) Difícil conseguir-se uma solução eficiente para todos os casos.
Paralelismo Explícito
• O paralelismo diz-se explícito quando cabe ao programador:
• Anotar as tarefas para execução em paralelo.
• Atribuir (possivelmente) as tarefas aos processadores.
• Controlar a execução indicando os pontos de sincronização.
• Conhecer a arquitetura dos computadores de forma a conseguir o máximo
desempenho (aumentar localidade, diminuir comunicação, etc).
• Vantagens e inconvenientes:
• (+) Programadores experientes produzem soluções muito eficientes para
problemas específicos.
• (–) O programador é o responsável por todos os detalhes da execução
(debugging pode ser deveras penoso).
• (–) Pouco portável entre diferentes arquiteturas
Programação paralela
• Programas de computador paralelos são mais difíceis
de programar que sequenciais, pois a concorrência introduz diversas
novas classes de defeitos potenciais, como a condição de corrida.
• A comunicação e a sincronização entre diferentes subtarefas é
tipicamente uma das maiores barreiras para atingir grande
desempenho em programas paralelos.
• O aumento da velocidade por resultado de paralelismo é dado pela lei
de Amdahl.
Computação paralela
• De uma forma simples, a computação paralela pode ser definida
como o uso simultâneo de vários recursos computacionais de forma a
reduzir o tempo necessário para resolver um determinado problema.
• Esses recursos computacionais podem incluir:
• Um único computador com múltiplos processadores.
• Um número arbitrário de computadores ligados por rede.
• A combinação de ambos.
Paralelismo versus concorrência
• Programação concorrente é um sinônimo de programação paralela,
mas com foco na interação entre as tarefas.
• A interação e a comunicação correta entre as diferentes tarefas, além
da coordenação do acesso simultâneo aos recursos computacionais
são as principais questões discutidas durante o desenvolvimento de
sistemas simultâneos.
Paralelismo versus concorrência
• Programação Paralela: vários programas/processos independentes
são executados em paralelo pelo computador sem interagir entre si
(não trocam informações).
• Programação Concorrente: UM programa (o próprio programa,
durante a sua execução) origina diferentes processos que irão
interagir entre si para realizar alguma tarefa. Implica o uso simultâneo
de recursos computacionais para resolver UM problema.
Noções de Sistemas Operacionais
• Para entender o paralelismo e a concorrência é preciso ter noções de
como ocorre a execução dos programas pelo computador
• Essa execução é comandada pelo sistema operacional
• Depende também da arquitetura do computador
Processo
• Um programa em execução em uma máquina
• Identificado pelo seu PID (Process Identifier)
• As informações sobre um processo estão na tabela de processos, que
é acessada pelo pid.
• Além do código executável carregado na memória, nós temos
• uma pilha de execução,
• um apontador para esta pilha,
• um contador de programa,
• valores dos registradores da máquina,
• outras informações.
Processo
• Um processador pode executar somente um processo a cada instante
• Em um SO multi-tarefa, processos se alternam, no uso do
processador – cada processo é executado durante uma parte de
tempo
• Se houver N processadores, N processos podem ser executados
simultaneamente
Processo
• Escalonamento de processos: O escalonador do SO seleciona o(s)
processo(s) que deve(m) ser executado(s) pelo(s) processador(es).
• Algoritmo de escalonamento: Define a ordem de execução de
processos com base em uma fila, prioridade, deadline, etc.
• Em geral, os sistemas adotam uma política para atender a todos os
processos de maneira justa e igualitária.
• Processos do sistema e aplicações críticas (um alarme, por exemplo)
exigem maior prioridade.
• Um processo interage com outros processos através de mecanismos
de comunicação.
Modelo de Processo

• Informações que dizem respeito a um processo:

espaço de endereçamento
arquivos abertos
processos filho AMBIENTE
sinais
estatísticas de uso
contador de programa (PC)
apontador de pilha (SP)
EXECUÇÃO
conjunto de registradores
estado de execução
Modelo de Processo
• Classificação dos modelos de processos quanto ao
custo de troca de contexto e de manutenção
• “heavyweight” (processo tradicional)
• “lightweight” (threads)
Modelo de Processo Tradicional - Heavyweight

• O processo é composto tanto pelo ambiente como pela execução:


• Processo tradicional: ambiente + execução.
• Cada processo possui um único fluxo de controle (contador de
programa) e roda de forma independente dos demais.
Modelo de Processo Tradicional
Heavyweight
Processo 1 Processo 1 Processo 1

main: main: main:


x = 1; x = 1; x = 1;
PC y = 2; y = 2; y = 2;
seta(); PC seta(); seta();
a = 4; a = 4; a = 4;
exit; exit; exit;
seta: seta: PC seta:
z = 3; z = 3; z = 3;
k = 4; k = 4; k = 4;

t1 t2 t3
Modelo de Processo Tradicional
• Como, em um dado instante, podem haver vários processos
ativos simultaneamente, o processador é chaveado entre os
diversos processos.
• Por esta razão, fica praticamente impossível prever o tempo de
execução de um processo, pois este dependerá da carga do
sistema.
Modelo de Processo Tradicional
• Todo sistema operacional deve possuir mecanismos que
permitam a criação de processos. Geralmente, um
processo somente é criado por outro processo, o que
nos leva a uma hierarquia em árvore.

init

...
getty getty shell

cmd1 cmd2 cmdn


Modelo de Processo Tradicional
• Um processo é criado a partir de:
• um programa (criação tradicional) (MS-DOS):
• create_process(“teste.exe”);
• outro processo (clonagem) (Unix):
• fork();
Processos
• A chamada do UNIX fork() cria um novo processo, denominado
processo filho
• O processo filho é uma cópia exata do processo que chama a rotina
fork(), sendo a única diferença um identificador de processo diferente
• Esse processo possui uma cópia das variáveis do processo pai
inicialmente com os mesmos valores do pai
• O processo filho inicia a execução no ponto da chamada da rotina
• Caso a rotina seja executada com sucesso, o valor 0 é retornado ao
processo filho e o identificador do processo filho é retornado ao
processo pai
Processos
• Os processos são “ligados” novamente através das chamadas ao sistema:
• wait(status): atrasa a execução do chamador até receber um sinal ou um dos seus processos filhos
terminar ou parar
• exit(status): termina o processo
• Criação de um único processo filho
pid = fork();
código a ser executado pelo pai e filho
if (pid == 0) exit (0); else wait (0)
• Filho executando código diferente
pid = fork();
if (pid == 0) {
código a ser executado pelo filho
} else {
código a ser executado pelo pai
}
if (pid == 0) exit (0); else wait (0);
Admitindo que os programas prog1 e prog2 imprimem
”sou o <nome-prog>”, o que o prog1 e prog2 vão imprimir?

prog1 prog2

main() main()
{ {
create_process(”prog2”,…); fork();
printf(”sou o prog1\n”); printf(”sou o prog2\n”);
} }
Estados do Processo
• Apesar dos processos serem relativamente auto suficientes, muitas
vezes eles necessitam acessar outros recursos (discos, terminais) ou
mesmo se comunicar com outros processos.
• Quando um processo está ocioso esperando que um evento
aconteça, nós dizemos que ele está bloqueado.
• Em algumas situações, o processo pode ser bloqueado a revelia pelo
sistema operacional.
Estados do Processo
• Os estados básicos de um processo são:
• rodando (running),
• bloqueado (blocked) e
• pronto (ready).
• Em um sistema monoprocessado, só temos um único processo
rodando a cada instante.
Estados do Processo
Rodando
(1) (4)
(3)
Bloqueado (2)
Pronto

(1) O processo bloqueia-se à espera de um evento


(2) O evento esperado pelo processo ocorreu. Ele pode agora se
executar. Passa então ao estado “pronto”
(3) O processo é escolhido para execução
(4) O tempo de posse do processador pelo processo esgotou-se.
O sistema operacional retira o processador do processo
Implementação de Processos
• Todas as informações sobre um processo são mantidas na tabela de
processos.
• A tabela de processos contem campos que dizem respeito à gerência
do processo, à gerência da memória e à gerência de arquivos.
• A tabela de processos possui uma entrada por processo e os campos
nela contidos variam de sistema operacional para sistema
operacional.
Tabela de Processos
• Dados referentes ao processo na tabela de processos:
• identificador do processo (pid),
• valor dos registradores,
• valor do contador de programa (PC),
• valor da palavra de estado (PSW),
• valor do apontador de pilha (SP),
• estado do processo,
• instante do início do processo,
• tempo de processador utilizado, etc.
Tabela de Processos
• Dados referentes à memória na tabela de processos:
• endereço do segmento de texto, dados e pilha, estado da saída,
informações sobre proteção, etc
• Dados referentes à gerência de arquivos na tabela de processos:
• diretório raiz, diretório de trabalho, descritores de arquivos abertos,
parâmetros de chamadas em andamento, etc
Implementando a Multiprogramação

• O processo p1 solicita uma operação de E/S. Ele é bloqueado


pelo sistema operacional à espera da conclusão da operação.
• As informações de p1 são atualizadas na sua tabela de
processos.
• O sistema operacional escolhe um dos processos na fila dos
prontos (e.g. processo p2) e o coloca para executar.
Implementando a Multiprogramação

• O resultado da operação solicitada por p1 chega.


• O hardware interrompe p2, salvando o seu estado de execução
(registradores, descritores, etc) na pilha.
• O hardware acessa um endereço de memória física específico que
contem o vetor de interrupções.
• O vetor de interrupções contem o endereço da rotina de tratamento
de interrupções geradas por cada classe de dispositivo (disco, floppy,
terminal, clock, etc).
Vetor de Interrupções

início da rotina
#00618 de tratamento de
interrupção do
dispositivo 1
#00650
#0063C Vetor de
#00618 Interrupções
endereço vetor #00500
interrupções #000230

MEMÓRIA FÍSICA
Implementando a Multiprogramação
• A rotina de tratamento de interrupção é executada pelo SO.
• Os registradores que foram empilhados pelo hardware são salvos
na tabela de processos do processo p2.
• A interrupção é tratada.
• O processo p1, que solicitou o serviço, é colocado na fila de
prontos.
• O SO acessa a entrada da tabela de processos do processo
escolhido e carrega o conteúdo da tabela nos registradores de
máquina (restauração).
• O processo escolhido reinicia a execução.
Implementando a Multiprogramação
• Troca de contexto: a operação de salvamento dos
registradores de um processo e posterior restauração
de registradores de outro processo é chamada de
troca de contexto. A troca de contexto permite a troca
de processador entre processos.

p1 CPU
p2 (1) 2 53 4
4 0 0
1 0
4 1
5 5
8 8
9

p1
(1)-restauração
SO p2 (2) (2)-salvamento
Escalonamento de Processos
• O algoritmo de escalonamento de processos é o responsável pela
determinação de qual processo (dentre os prontos) vai rodar e por
quanto tempo.
• O algoritmo de escalonamento define, assim, a política de utilização
do processador pelos processos.
• Quando um processo solicita operações blocantes (E/S, por exemplo),
sua execução fica suspensa até que o evento solicitado ocorra.
Escalonamento de Processos
• Execução de 2 processos sem concorrência:
DIAGRAMA DE GANTT
exec idle exec idle exec P1
P2
idle exec idle exec
tempo
• Execução de 2 processos com concorrência:
exec idle exec idle exec P1

P2
idle exec idle exec
• Executando vários processos concorrentemente,
obtemos uma melhor utilização da CPU
Critérios do Escalonamento
• Ao se projetar um escalonador, devemos observar vários
critérios que devem estar presentes em um bom algoritmo
de escalonamento:
– Justiça (fairness): garantir que todos os processos do sistema terão
chances justas de uso do processador (chances iguais é muito
forte!!)
– Eficiência: quando houver trabalho a fazer, o processador deve
estar ocupado
– Minimizar o tempo de resposta: minimizar o tempo de resposta dos
usuários interativos.
Critérios de Escalonamento
• Minimizar o turnaround: O tempo de turnaround é o tempo que vai
desde o lançamento do processo até o seu término.
• É a soma dos seguintes componentes:
• tempo de espera por memória,
• tempo de espera pelo processador,
• tempo de espera por I/O e
• tempo de utilização da CPU.
• Este critério visa minimizar a espera de resultados. Mais utilizado em
processamento batch.
• Minimizar o waiting time: Este critério visa minimizar o tempo de
espera pela CPU.
• Maximizar o throughput: maximizar o número de jobs executados
em uma unidade de tempo
Critérios de Escalonamento
• Infelizmente, a maioria destes critérios é contraditório:
– minimizar o turnaround x minimizar o tempo de resposta:
– para que os usuários interativos obtenham um tempo de resposta
pequeno, geralmente os usuários batch são penalizados com um tempo de
execução maior e vice-versa.
– Um algoritmo que maximiza o throughput geralmente não é justo
com os processos de execução demorada.
Critérios de Escalonamento
• Infelizmente, a maioria destes critérios é contraditório:
– minimizar o turnaround x minimizar o tempo de resposta: para que os usuários
interativos obtenham um tempo de resposta pequeno, geralmente os usuários
batch são penalizados com um tempo de execução maior e vice-versa.
– Um algoritmo que maximiza o throughput geralmente não é justo com os
processos de execução demorada.
Classificação dos Escalonadores Quanto à Preempção

• Preempção: suspensão temporária da execução de um processo


• Os escalonadores podem ser:
• Preemptivos
• Não-Preemptivos
Escalonadores Não-Preemptivos
• Escalonador não-preemptivo:Quando um processo obtem o
processador, ele roda até o fim ou até que ele peça uma operação
que ocasione o seu bloqueio.
• Nenhuma entidade externa “tira a CPU à força” do processo
Escalonadores Preemptivos
• Cada processo possui um tempo máximo de
permanência de posse do processador. Quando este
tempo se esgota, o SO retira o processador deste
processo e permite que outro processo se execute.
• Como controlar o tempo de execução do processo?
• Todo processador moderno possui um clock que gera
interrupções em uma frequência determinada. Cada
uma destas interrupções é chamada clock tick.
Escalonadores Preemptivos
• O SO mantem um contador que é decrementado a cada clock tick.
• Se o contador chegar a 0, o tempo de permanência do processo
acabou.
• O valor inicial deste contador corresponde ao tempo máximo de
permanência do processo com a CPU e denomina-se time slice.
Escalonadores Preemptivos x
Escalonadores Não-Preemptivos
• Os escalonadores não-preemptivos são de projeto extremamente
simples, porém permitem que um processo detenha a CPU por um
tempo arbitrário.
• Neste caso, um processo pode obter o monopólio do processador,
impedindo os outros processos de rodarem.
• Isso viola vários dos critérios de um bom escalonador (justiça, tempo
mínimo de resposta, etc).
Escalonadores Preemptivos x
Escalonadores Não-Preemptivos
• Os escalonadores preemptivos asseguram um uso mais balanceado
da CPU e são utilizados na maioria dos SO modernos.
• Porém, o projeto de tais escalonadores, além de ser complexo,
introduz complicações na programação de processos concorrentes.
• Como agora os processos podem ser interrompidos em um tempo
arbitrario, eles devem proteger suas estruturas de dados contra a
interferência de outros processos (regiões críticas).
a=0
b=0
P1 P2
… …
read(…); read(…);
a=1; a=2;
b=1; b=2;
read(…); read(…);

Não-preemptivo: Preemptivo:
P1->P2 a=? b=? P1->P2 a=? b=?
Algoritmos de Escalonamento
• O problema a ser resolvido pelos algoritmos de escalonamento é o
seguinte: dado um conjunto de processos que devem ser executados,
como dividir a utilização do processador entre estes processos?
• Algoritmos de escalonamento:
• First Come First Served
• Round-Robin
• Prioridades
• Shortest Job First
First Come First Served (FCFS)

• O processo obtem a CPU de acordo com a ordem de chegada das


solicitações. O processo que pede a CPU primeiro obtem a CPU em
primeiro lugar.
• O escalonamento FCFS é não-preemptivo. Assim, um processo
CPU/bound pode fazer com que vários processos esperem por um
tempo indeterminado.
First Come First Served (FCFS)
• Implementação: Os processos que solicitam a CPU são colocados em
uma fila de prontos, que é gerenciada segundo a política FIFO.

Pede I/O
B
A
E E
D D
C C
B CPU
Round-Robin
• Cada processo tem o direito de usar o processador por um intervalo
de tempo pré-definido. Este intervalo de tempo é denominado
quantum. Quando o quantum se esgota, o processador é dado a
outro processo.
Algoritmo justo
Round-Robin

• Funcionamento: Esgotou
quantum
A
E D C B CPU

t1
B
A E D C CPU

t2
Round-Robin
• Um dos maiores problemas do algoritmo de escalonamento round-
robin diz respeito à determinação do valor a ser atribuído ao
quantum. Para a determinação deste valor, devemos levar em
consideração o tempo médio da troca de contexto e o tempo de
resposta desejado.
• normalmente, o quantum fica em torno de 100 ms.

Robin
Escalonamento com Prioridades
• Baseia-se no fato de que alguns processos são prioritários e, assim,
devem ser executados antes dos outros.
• A cada processo é atribuída uma prioridade. Processos com
prioridade maior rodam primeiro.
Escalonamento com Prioridades
• Como atribuir as prioridades?
• de forma estática: os processos são divididos em classes e a
cada classe é atribuída uma prioridade. A cada prioridade
existe uma fila de prontos associada
30 E D C B A
t1 20 G F
CPU
10 I H

30 A E D C B
t2 20 G F
CPU
10 I H
Escalonamento com Prioridades
• de forma dinâmica: o sistema analisa o comportamento dos
processos e atribui prioridades favorecendo um certo tipo de
comportamento.
• Exemplo: processos I/O bound devem possuir prioridade alta.
• Prioridade dinâmica: 1/f, onde f é a fração do quantum de tempo usada na
última rodada do processo.
Shortest Job First
• Algoritmo projetado para sistemas batch
• Objetivo: reduzir o tempo de turnaround
• Requer que o tempo total de execução do job seja conhecido antes
do início da execução
• Algoritmo: Dado um conjunto de jobs prontos, execute os jobs com
menor tempo de execução antes.
Shortest Job First

• Funcionamento:

8 4 4 4
SJF E D C B
ta=20 ta=12 ta=8 ta=4

4 4 4 8
FCFS B D C E
ta=20 ta=16 ta=12 ta=8
SJF
4 taB=4
B
Tamedio= (4+8+12+20)/4=11
4 taC=8
C
4 taD=12
D
8 taE=20
E

tempo
Tamedio= (8+12+16+20)/4=14
FCFS
4 taB=20
B
4 taC=12
C
4 taD=16
D
8 taE=8
E

tempo
Shortest Job First
• Aplicação para sistemas interativos:
• Processo interativo:
espera comando
executa comando
• Se considerarmos cada “executa comando” como um job, podemos
aplicar o SJF para processos interativos.
Shortest Job First
• Problema: Como determinar o tempo de execução do comando?
• Usar a técnica conhecida como aging,que estima um valor baseando-se e
lembrando-se dos valores passados.
• Quando um valor fica muito antigo, ele praticamente não influencia mais na
estimativa.
• Exemplo: T0: tempo médio de execução de um comando, T1: tempo
medido na última rodada
• Tempo estimado = T
• T = aT1 + (1-a) T0
• O que acontece se a=1/2?
Escalonamento em Dois Níveis
• Um caso típico de escalonamento em dois níveis é o algoritmo que
considera tanto os processos que estão em memória como os processos
que estão em disco.
• Primeiro nível: manipula os processos que estão carregados em memória.
• Pode ser usada uma das 4 classes de algoritmos descritas anteriormente
• Segundo nível: examina periodicamente o tempo de execução dos processos
e os tira ou os carrega em memória (operações de swap in/ swap out).
• Quando o modelo de processos inclui threads, podemos ter também
um algoritmo de 2 níveis. O primeiro nível determina que processo irá
rodar e o segundo nível determina qual thread do processo
selecionado irá executar.
Comunicação entre Processos
• Ao longo de sua execução, um processo necessita frequentemente
interagir com outros processos.
• A interação entre os processos pode ser de 2 maneiras:
– competição
– cooperação
Competição entre Processos
• Neste caso, os processos entram em conflito pela utilização de
recursos.
• O processo que utiliza um recurso deve sempre deixá-lo em estado
consistente, pois o mesmo poderá ser utilizado por vários outros
processos que se desconhecem mutuamente.
• As relações de competição afetam todos os processos que
executam em um mesmo computador.
• São ditas relações mínimas entre os processos.
Cooperação entre Processos
• Neste caso, os processos que interagem entre si possuem
conhecimento da existência de outros processos.
• Tipos de Cooperação:
• Por Compartilhamento de variáveis
• Por Troca de mensagens
Cooperação entre Processos
• Cooperação por compartilhamento:
• os processos não se conhecem explicitamente, interagindo
entre si através de variáveis compartilhadas.
• Cooperação por troca de mensagens:
• Neste caso, os processos enviam explicitamente valores a
outros processos. As primitivas de comunicação existentes
são enviar e receber. A comunicação só se completa quando
o par de primitivas enviar-receber é executado.
• A cooperação entre processos é descrita
frequentemente na literatura como IPC (inter-process
communication).
Exemplos

Memória Compartilhada Troca de Mensagens


a=0
P1 P2 P1 P2
… … … …
a=15; print(a); send(P2, 15); receive(P1, &a);
… … … print(a);

15 15
Condições de Corrida
• Como o sistema operacional determina, através da política de
escalonamento, o processo que vai rodar e por quanto tempo, não
sabemos a priori em que ordem dois processos ativos irão se
executar.
• Condições de corrida: Acontecem quando dois ou mais processos
acessam concorrentemente as mesmas posições de memória.
• Se o valor final contido nestas posições depende da ordem na qual os
processos foram executados, estamos diante de uma condição de corrida
(race condition).
• A existência de condições de corrida em um sistema pode levar a
resultados inesperados, devido ao não-determinismo inerente a estas
condições.
Exemplos - Condição de Corrida
a=0
P1 P2 P1 -> P2 print(15);
… … P2 -> P1 print(0);
a=15; print(a); condição de corrida
… …
a=0
P1 P2
P1 -> P2 a=20;
… …
P2 -> P1 a=15;
a=15; a=20;
condição de corrida
… …
a=0
P1 P2 P1 -> P2 0 0;
… … P2 -> P1 0 0;
print(a); print(a); Não há condição de corrida
… …
Condições de Corrida
• Primeiro exemplo de condições de corrida:
– Considere dois processos A e B com o seguinte código:

Processo A Processo B
x=x+1; x=x+1;

– Considere que, inicialmente, o valor de x=0. Que valores


de x podemos obter quando os processos A e B se
executam simultaneamente? (Suponha um escalonador
preemptivo *).
(*) Escalonadores preemptivos são algoritmos que permitem que um processo seja interrompido
durante sua execução, que seja por força de uma interrupção de entrada/saída, quer seja em
decorrência da politica de escalonamento adotada e aplicada por parte do escalonador de processos
ou simplesmente por força do término da execução do processo.
Caso 1: Processo A -> Processo B
Memória
Processo A Processo B

x 0
12
LOAD x,R1 (x=0)
INC R1
STOR x,R1 (x=1) tab. proc.
pid … R1…
----quantum--- 1
PA … 70…
tempo

LOAD x,R1 (x=1) PB … 18…


INC R1
STOR x,R1 (x=2)
CPU
R1
1
210
18
70
Valor final: x=2
Caso 2:
Processo A -> Processo B -> ProcessoMemória
A
Processo A Processo B
x 0
1
LOAD x,R1 (x=0)
INC R1
----quantum--- LOAD x,R1 (x=0)
tab. proc.
tempo

pid … R1…
INC R1 PA … 70…
1
STOR x,R1 (x=1) PB … 18…
1
STOR x,R1 (x=1)
----quantum---
CPU
R1
Valor final: x=1 18
0
70
1
• Esta situação pode ocorrer? Se sim, em que condições?
• Voce considera que o programador admitia, ao fazer o
seu programa, que a variavel x pudesse assumir o valor
1 ao final da execução?
Condições de Corrida
• Segundo exemplo de condições de corrida:
– Para que um arquivo seja impresso, deve-se colocar o seu
nome em uma fila em memória. Esta fila é controlada por
duas variáveis: out, que indica a posição onde o nome do
arquivo deve ser escrito e in, que indica o arquivo que deve
ser impresso no momento.
– O processo que deseja imprimir arquivos recupera a variável
out, coloca o nome do arquivo na posição indicada por out e
incrementa out de uma posição.
Condições de Corrida
Segundo Exemplo
in=8 out=10

Processo A Processo B
8 teste.c
9 cria.c escreve escreve
(arq.a,&out); (arq.b,&out);
10
out=out+1; out=out+1;

processo A: escreve arquivo


arq.a na fila de impressão
processo B: escreve arquivo
arq.b na fila de impressão

processo spool: imprime


Caso 1: Processo A -> Processo B
Processo A Processo B
LOAD out,R1
Fila de out(=10)
Impressão (coloca arq.a na
posição 10)

tempo
8 teste.c INC R1
STOR out,R1
9 cria.c (out=11) LOAD out,R1
10 arq.a (out=11)
11 arq.b (coloca arq.b na
posição 11)
INC R1
STOR out,R1
(out=12)
Valores Finais
in=8 out=12
Caso 2:
Processo A -> Processo B -> Processo A
Processo A Processo B
LOAD out,R1 (out=10)
Fila de (coloca arq.a na
Impressão posição 10)

LOAD out,R1 (out=10)


tempo
8 teste.c (coloca arq.b na
posição 10)
9 cria.c INC R1
10 arq.b STOR out,R1 (out=11)

11 INC R1
STOR out,R1
(out=11)

Valores Finais
in=8 out=11
Condições de Corrida
• As condições de corrida levam geralmente a resultados inesperados e,
por isso, devem ser evitadas.
• O que nós precisamos é de um meio de garantir que uma porção de
código seja executada somente por um processo de cada vez.
Fazemos, assim, uma serialização da execução.
Condições de Corrida
• Esta exclusividade na execução de partes de código é garantida por
mecanismos que implementam a exclusão mútua.
• A parte do código protegida pelos mecanismos de exclusão mútua é
denominada seção crítica.
• No exemplo 1, a seção crítica é simplesmente a instrução x=x+1. E no
exemplo 2?
Modelos de Processos
• Classificação dos modelos de processos quanto ao custo
de troca de contexto e de manutenção
“heavyweight”
“lightweight”
• Modelo de processos tradicional (heavyweight)
– Neste caso, o processo é composto tanto pelo
ambiente como pela execução.
– Itens da tabela de processos:
espaço de endereçamento
( código, dados, pilha) AMBIENTE
arquivos abertos
conjunto de registradores
(PC, SP, Uso geral, etc) EXECUÇÃO
estado de execução
Tabela de Processos (heavyweight)
Pid Ppid IniMem TamMem Arquivos Command… R1 R2 … PC SP Estado
35
178

Tabelas de Threads

Tid R1 R2… PC SP Estado


Tabela de Processos (lightweight)
0
1
Pid Ppid IniMem TamMem Arquivos Command …
35
178
Tid R1 R2… PC SP Estado

0
1

Execução com Processos

P P2
PC
código 1 código
PC

• A troca de contexto entre processos tradicionais é pesada para o


sistema. Neste caso, o contexto é o ambiente e o estado de
execução do processo.
• Normalmente, os processos tradicionais não compartilham memória.
• O processo tradicional é composto de uma única thread de controle
Processos Leves - Threads
• As linhas de controle múltiplas foram criadas para permitir
maior concorrência na execução dos processos.
• Neste modelo, a entidade processo é dividida em duas
entidades: processo (ou tarefa) e thread.
• O processo corresponde ao ambiente e a thread
corresponde ao estado de execução.
• Um processo é composto por várias threads que
compartilham o ambiente (memória, descritores de
arquivos, etc).
Processos Leves - Threads
Processos Leves - Threads
Processos Leves - Threads
espaço de endereçamento
( código, dados, pilha) AMBIENTE (TABELA DE PROCESSOS)
arquivos abertos
conjunto de registradores
(PC, SP, Uso geral, etc) EXECUÇÃO (TABELA DE THREADS)
estado de execução

No modelo leve, existem duas entidades:


Processo: armazena informações de ambiente
Thread: armazena informações de execução
Um processo possui pelo menos uma thread.
Cada processo possui uma tabela de threads associada
Execução com Threads

P
código 1 PC
PC (thread2)
(thread1)

• No exemplo acima, temos 1 processo com 2 threads de controle.


• Uma thread pode se bloquear à espera de um recurso. Neste momento, uma
outra thread (do mesmo processo ou não) pode se executar.
• A troca de contexto é mais leve. Se a thread t1 do processo p1 estiver rodando
e entrar em execução, logo após, a thread t2 também do processo p1, a troca
de contexto só diz respeito à execução. O ambiente continua o mesmo.
Estados das Threads
• A entidade que realmente se executa é a thread.
O processo (ou tarefa) é só o ambiente.
• Estados de execução das threads:

ready running

blocked

As threads compartilham as variáveis globais do programa, os descritores


abertos, etc. Assim, há necessidade de mecanismos de sincronização
int a; int a;
main() main()
{ {
rot1(); t1=create_thread(rot1());
rot2(); t2=create_thread(rot2());
printf(”a=%d\n”, a); thread_join(t1);
} thread_join(t2);
rot1() printf(”a=%d\n”, a);
{ }
a=10; rot1()
} {
rot2() a=10;
{ }
a=20; rot2()
} {
a=20;
}
Modelo de Execução das Threads
• O modelo de execução determina como as threads
irão se organizar para resolver um problema.
• É claro que esta organização é altamente dependente
do problema a ser resolvido.
• Em servidores, o modelo pode ser:
• Threads dinâmicas: uma thread é criada para tratar
cada requisição
• Threads estáticas: o número de threads é fixo
• dispatcher/worker, team e pipeline
• Nos modelos a seguir, uma thread trata uma
requisição
Modelo Dispatcher/Worker
– Nesta organização, a thread dispatcher
recebe todas as requisições.
– Ela escolhe, então, uma thread trabalhadora e a
“acorda” uma thread ociosa para tratar a
requisição. A thread trabalhadora executa a
solicitação e, quando acaba, sinaliza o dispatcher.

- Consumo rápido de msgs


- Boa distribuição das
dispatcher

requisições
mailbox

s1 s2 sn

- Flexibilidade: podemos
worker

worker

worker
facilmente mudar o número
msg threads
PROCESSO
Modelo Team

– Nesta organização, cada thread é inteiramente


autônoma. Todas as threads acessam a caixa-
postal, obtém requisições e as executam.

- Bom consumo de msgs


thread

thread

thread

thread
- Boa distribuição das
requisições
- Flexibilidade: podemos
mailbox
facilmente mudar o número
PROCESSO
threads
Modelo Pipeline
– Nesta organização, cada thread tem uma
tarefa específica e os dados de entrada de
uma thread são produzidos pela thread
anterior.

- Consumo rápido de msgs


thread

thread

thread

thread
- Aplicações produtor/
consumidor
-Menos flexível que as
mailbox
abordagens anteriores
PROCESSO
Gerência de Processos Distribuídos
Comparação entre os modelos de processos

• Modelos:
– Processos tradicionais: Não há concorrência no interior do processo.
Existem chamadas de sistema bloqueadas
– Processos + threads: Há concorrência no interior do processo. Existem
chamadas de sistema bloqueadas
– Máquina de estados finitos: Neste modelo, são guardados os estados
parciais de execução de solicitações. Não há chamadas de sistema
bloqueadas.

• Os sistemas que utilizam processos tradicionais são programados de maneira


relativamente simples, porém não permitem uma grande concorrência. Os
sistemas à máquina de estados permitem concorrência no tratamento de
requisições porém sua programação é complexa. O modelo de threads foi
proposto para proporcionar concorrência e facilidade de programação ao
mesmo tempo.
Gerência de Processos Distribuídos
Aspectos no projeto das threads
 Tipos de threads:
– Estáticas: o número de linhas de controle que comporão um processo é determinado
em tempo de compilação. A execução começa com n threads.
– Dinâmicas: as threads são criadas e destruídas dinamicamente, ao longo da execução.
As threads são criadas através de primitivas. A execução começa com uma thread
apontando para o início da rotina main.
 técnica mais utilizada
 Sincronização:
– Como as threads compartilham memória, elas necessitam de mecanismos de
sincronização. A maioria dos pacotes de threads oferece dois mecanismos: variáveis
mutex e variáveis de condição.
• variáveis mutex: semáforo binário
– dois estados: fechado ou aberto
– operações: lock, unlock, trylock
• variáveis de condição: uma variável de condição é sempre associada a um mutex
(semáforo).
– semântica: obtem o semáforo, testa a condição. Se a condição for
verdadeira, continua. Senão, solta o semáforo e fica esperando na condição.
ESTATICA DINAMICA

#NTHREADS 3 main()
#THREAD0 rot1 {
#THREAD1 rot2 …
#THREAD2 rot1 create_thread(rot2,…);
rot1() …
{ create_thread(rot3,…);

rot3(); }
} rot2()
rot2() {
{ }
} rot3()
rot3() {
{ }
}
Gerência de Processos Distribuídos
Aspectos no projeto das threads
• Exemplo de variáveis de condição: Scanner daemon

pager scanner daemon


Processo
Memory Manager

MEMÓRIA
memory_ FÍSICA
occupation

• Scanner (paginador): • Pager (alocador de páginas):


pthread_mutex_lock(v); pthread_mutex_lock(v);
while (memory_occupation < M_BOUND) memory_occupation++;
pthread_cond_wait( if (memory_occupation >=
memory_full, v); M_BOUND)
livres = libera_paginas(); pthread_cond_signal(me
memory_occupation = memory_occupation - livres; mory_full);
pthread_mutex_unlock(v); pthread_mutex_unlock(v);
Gerência de Processos Distribuídos
Aspectos no projeto das threads

 Tratamento de variáveis globais:


– Como as variáveis globais são acessadas agora por
diversas threads, deve-se pensar em uma maneira de se
• Proteger o acesso às variáveis globais
garantir a não interferência entre as mesmas.
com mecanismos de sincronização. Não é
– Soluções: uma solução genérica, pois algumas
• Não permitir variáveis globais. Não é boa porque variáveis globais são setadas pelo próprio
vários softwares, inclusive o Unix, utilizam sistema (e. g. errno).
variáveis globais • Colocar as chamadas ao sistema que
• Criar um conjunto de variáveis globais para cada setam as variáveis globais em uma única
thread (variáveis globais privadas) que só são thread. Não é uma boa solução pois
vistas pela thread que as criou e pelos outros implica a perda de concorrência.
processos do sistema. Utilizar um conjunto de
 Ainda não há uma boa solução para este
primitivas: create_global, read_global, set_global.
problema!
– Não é uma boa solução pois aumenta a
complexidade da programação e a
complexidade da semântica das threads,
#include<errno.h> /* errno = DISK_FULL */
rot1() /*thread 1*/
{

if (msgrcv(idfila, …) == -1)
printf(”erro na fila = %d\n”, errno);

}

rot2() /*thread 2*/


{

if (write(fd, …) == -1)
printf(”erro no write = %d\n”, errno);

}
Gerência de Processos Distribuídos
Implementação das threads

• Em que nível implementar as threads?


 Implementar o modelo de processos e threads à nível de sistema
operacional, criando as abstrações de processo e de thread.
 Manter o modelo de processos heavyweight e simular as múltiplas threads
através de biblioteca.
• Implementação de threads à nível de sistema operacional
– O Kernel do sistema operacional é responsável pela criação das threads, seu
escalonamento e seu término.
– Existência de uma tabela de threads no kernel, que contem os dados
inerentes às threads.
– As threads são manipuladas através de chamadas ao sistema.
– Quando uma thread é bloqueada, o kernel escolhe uma outra thread para
rodar (do mesmo processo ou de processos diferentes).
– Também são chamadas “heavyweight threads” porque o tempo envolvido
na troca de contexto é considerável, apesar de menor do que o tempo de
troca de contexto entre processos.
Implementação no kernel

Usuário

fila ready
SO
T35,0 T78,0

Tabelas de Threads
Tid R1 R2… PC SP Estado
Tabela de Processos 0 READY
1 BLOQ
Pid Ppid IniMem TamMem Arquivos Command …
35
78
… Tid R1 R2… PC SP Estado
0 READY
1 RUN

Gerência de Processos Distribuídos
Implementação das threads

• Implementação de threads à nível de biblioteca


– As threads são simuladas no espaço do usuário. O sistema operacional
continua tratando com processos comuns.

thread n-1
thread 0

thread 1
ambiente de execução (runtime)
kernel

– O ambiente de execução é o responsável pelo gerenciamento das linhas de


controle.
– Existência de uma tabela de threads no ambiente de execução, que contem
os dados inerentes às threads.
– As threads são manipuladas através de chamadas à funções.
Implementação no modo usuário (biblioteca)

Tabela de Threads
P35
fila ready Tid R1 R2… PC SP Estado
0 READY
T0 1 RUN

Usuário

fila ready SO

P78

Pid Ppid IniMem TamMem Arquivos Command… R1 R2 … PC SP Estado


35 31 65 400 50 RUN
78 14 234 200 100 READY

Gerência de Processos Distribuídos
Implementação das threads a nível de usuário

– Troca de contexto extremamente rápida.


– Cada processo pode ter o seu próprio algoritmo de escalonamento.
– Problema: As chamadas de sistema bloqueadas causam o bloqueio
de todas as threads do processo! Por essa razão, elas não podem ser
feitas diretamente pelas threads.
– Solução: Colocar uma capa (jacket) sobre as chamadas blocantes ao
sistema. Com essa técnica, as chamadas blocantes são
“mascaradas”pela biblioteca de threads, que faz um teste de
bloqueio. Se a chamada ao sistema resultar em bloqueio, ela só é
feita se não houver nenhuma thread esperando. Se houver alguma
thread esperando para se executar, a thread anterior é bloqueada e a
nova thread entra em execução. jacket: inserção de código de
verificação às chamadas. Só funciona com bibliotecas standard.
Gerência de Processos Distribuídos
Implementação das threads

• Análise dos tipos de implementação


– “As threads foram introduzidas para dar mais concorrência à
execução das aplicações a um custo relativamente baixo”.
– Implementação à nível de kernel: alta concorrência potencial porém
o custo de troca de contexto entre threads continua alto.
– Implementação à nível de usuário: pouca concorrência devido ao
bloqueio de todas as threads do processo quando uma delas faz
operações de I/O. O custo da troca de contexto é extremamente
baixo.
• Conclusão: Ainda não existe uma boa implementação de threads.
• Situação atual: A maioria dos sistemas implementa threads a nível de
usuário, por questões de comodidade (não é necessário alterar o SO) ou
desempenho. Os poucos sistemas que implementam threads a nível de
sistema operacional (e.g. Mach) oferecem também uma interface mais
amigável através de um conjunto de bibliotecas C (C threads) e, estas
sim, fazem as chamadas ao sistema.

Você também pode gostar