Upload
others
View
2
Download
0
Embed Size (px)
Citation preview
Sistemas Operacionais
Programação ConcorrenteSincronização entre processos
Edson [email protected]
http://www.inf.pucrs.br/~emoreno
Sumário
Conceituação
Princípios de concorrência
Região crítica
Coordenação de Acesso à Região crítica
Introdução Threads são Processos leves
Definidos para permitir paralelismo em um programa de aplicação
As threads possuem Sua própria pilha de execução e program counter
Cada uma roda um código seqüencial (definido no programa de aplicação)
Podem criar processos (threads) filhos
Compartilham o mesmo espaço de endereçamento (dados globais) do processo pesado
Portanto, Quando uma thread altera um valor compartilhado, todas percebem esta mudança
Arquivos abertos por uma thread são disponível as outras do mesmo processo pesado.
Como manter a coerência dos dados? A partir do emprego de mecanismos de sincronização
Sumário
Conceituação
Princípios de concorrência
Região crítica
Coordenação de Acesso à Região crítica
Princípios de concorrência Exemplo 1
Algoritmo do produtor consumidor
Produtor: processo que gera n valores
Consumidor: consome os dados gerados pelo produtor
Meio de armazenamento: fila circular
Produtor Consumidor
Princípios de concorrência Exemplo 1 - Produtor consumidor
ProdutorConsumidor
#define N 10 #
int buffer[N], addrprod = 0, addrcons = 0 ;
void * produtor(){
int i, p=50;
for(i=0; i<20; i++) {
while(((addrprod + 1) % N) == addrcons){}
p++;
buffer[addrprod] = p ;
addrprod= ( addrprod + 1 ) % N ;
}
}
void * consumidor(){
int i, c;
for (i=0; i<20; i++){
while(addrprod == addrcons) {}
c = buffer[addrcons] ;
printf("Consumi o valor %d”,c);
printf(“Posição consimida %d”, addrcons);
addrcons= ( addrcons + 1 ) % N ;
}
}
Princípios de concorrência Exemplo 1 - Produtor consumidor
Cenário de execução
Se o consumidor for executado primeiro
Buffer está vazio
Produtor ainda não gerou valores
Processador ficará preso fazendo while
Produtor Consumidor
void * consumidor(){
int i, c;
for (i=0; i<20; i++){
while(addrprod == addrcons) {}
c = buffer[addrcons] ;
printf("Consumi o valor %d”,c);
printf(“Posição consimida %d”, addrcons);
addrcons= ( addrcons + 1 ) % N ;
}
}
Princípios de concorrência Exemplo 1 - Produtor consumidor
Produtor Consumidor
void * produtor(){
int i, p=50;
for(i=0; i<20; i++) {
while(((addrprod + 1) % N) == addrcons){}
p++;
buffer[addrprod] = p ;
addrprod= ( addrprod + 1 ) % N ;
}
}
Cenário de execução
Quando o produtor for executado
Executa o comando while
Começa a inserir valores no buffer
Posição de deposito é incrementado
Tratamento para buffer circular
Processo se repete até a thread perder o
processador
Sumário
Conceituação
Princípios de concorrência
Região crítica
Coordenação de Acesso à Região crítica
Região crítica
Ocorre quando
Sistema composto por N processos (onde N > 1)
Cada processo executa seu próprio código independente dos demais
Processos compartilham um dado recurso
O estado de um processo é desconhecido pelo outro
O estado/valor do recurso pode ser alterado
Região crítica Exemplo
Thread 0 Thread 1a
void * t0(){
long i ;
for (i=0; i<1000000; i++){
a = a + 5 ;
}
printf("Encerrei a t0 %d\n",sizeof(int));
}
void * t1(){
long i ;
for (i=0; i<1000000; i++){
a = a + 2;
}
printf("Encerrei a t1\n");
}
Região crítica Exemplo
Supondo a com valor inicial 60 e a seguinte linha de tempo
T0: Thread0 executa load a e perde o processador: o valor do acumulador (60) é salvo.
Thread 0 Thread 1a = 60
PRESSUPONDO O SEGUINTE ASSEMBLY GERADO
load a ; // carregar a no acumulador
add 5 ; // adicionar 5 ao acumulador
store a ; // armazenar na variável a
PRESSUPONDO O SEGUINTE ASSEMBLY GERADO
load a ; // carregar a no acumulador
add 2 ; // adicionar 2 ao acumulador
store a ; // armazenar na variável a
Região crítica Exemplo
Supondo a com valor inicial 60 e a seguinte linha de tempo
T0: Thread0 executa load a e perde o processador: o valor do acumulador (60) é salvo.
T1: Thread1 ganha o processador e executa load a: o valor do acumulador é 60 .
Thread 0 Thread 1a = 60
PRESSUPONDO O SEGUINTE ASSEMBLY GERADO
load a ; // carregar a no acumulador
add 5 ; // adicionar 5 ao acumulador
store a ; // armazenar na variável a
PRESSUPONDO O SEGUINTE ASSEMBLY GERADO
load a ; // carregar a no acumulador
add 2 ; // adicionar 2 ao acumulador
store a ; // armazenar na variável a
Região crítica Exemplo
Supondo a com valor inicial 60 e a seguinte linha de tempo
T0: Thread0 executa load a e perde o processador: o valor do acumulador (60) é salvo.
T1: Thread1 ganha o processador e executa load a: o valor do acumulador é 60 .
T2: Thread1 executa add 2: o valor do acumulador é 62.
Thread 0 Thread 1a = 60
PRESSUPONDO O SEGUINTE ASSEMBLY GERADO
load a ; // carregar a no acumulador
add 5 ; // adicionar 5 ao acumulador
store a ; // armazenar na variável a
PRESSUPONDO O SEGUINTE ASSEMBLY GERADO
load a ; // carregar a no acumulador
add 2 ; // adicionar 2 ao acumulador
store a ; // armazenar na variável a
Região crítica Exemplo
Supondo a com valor inicial 60 e a seguinte linha de tempo
T0: Thread0 executa load a e perde o processador: o valor do acumulador (60) é salvo.
T1: Thread1 ganha o processador e executa load a: o valor do acumulador é 60 .
T2: Thread1 executa add 2: o valor do acumulador é 62.
T3: Thread1 executa store a: o valor do acumulador é armazenado na variável.
Thread 0 Thread 1a = 62
PRESSUPONDO O SEGUINTE ASSEMBLY GERADO
load a ; // carregar a no acumulador
add 5 ; // adicionar 5 ao acumulador
store a ; // armazenar na variável a
PRESSUPONDO O SEGUINTE ASSEMBLY GERADO
load a ; // carregar a no acumulador
add 2 ; // adicionar 2 ao acumulador
store a ; // armazenar na variável a
Região crítica Exemplo
Supondo a com valor inicial 60 e a seguinte linha de tempo
T0: Thread0 executa load a e perde o processador: o valor do acumulador (60) é salvo.
T1: Thread1 ganha o processador e executa load a: o valor do acumulador é 60 .
T2: Thread1 executa add 2: o valor do acumulador é 62.
T3: Thread1 executa store a: o valor do acumulador é armazenado na variável.
T4: Thread0 ganha o processador, o valor do acumulador é restaurado (60) e executa a
operação add 5: o valor do acumulador passa a ser 65.
Thread 0 Thread 1a = 62
PRESSUPONDO O SEGUINTE ASSEMBLY GERADO
load a ; // carregar a no acumulador
add 5 ; // adicionar 5 ao acumulador
store a ; // armazenar na variável a
PRESSUPONDO O SEGUINTE ASSEMBLY GERADO
load a ; // carregar a no acumulador
add 2 ; // adicionar 2 ao acumulador
store a ; // armazenar na variável a
Região crítica Exemplo
Supondo a com valor inicial 60 e a seguinte linha de tempo
T0: Thread0 executa load a e perde o processador: o valor do acumulador (60) é salvo.
T1: Thread1 ganha o processador e executa load a: o valor do acumulador é 60 .
T2: Thread1 executa add 2: o valor do acumulador é 62.
T3: Thread1 executa store a: o valor do acumulador é armazenado na variável.
T4: Thread0 ganha o processador, o valor do acumulador é restaurado (60) e executa a
operação add 5: o valor do acumulador passa a ser 65.
T5: Thread0 executa store a o valor do acumulador é armazenado na variável a (60).
Thread 0 Thread 1a = 65
PRESSUPONDO O SEGUINTE ASSEMBLY GERADO
load a ; // carregar a no acumulador
add 5 ; // adicionar 5 ao acumulador
store a ; // armazenar na variável a
PRESSUPONDO O SEGUINTE ASSEMBLY GERADO
load a ; // carregar a no acumulador
add 2 ; // adicionar 2 ao acumulador
store a ; // armazenar na variável a
Região crítica Solução para o problema
Emprego de protocolos de entrada e saída na região crítica
Sinaliza-se a entrada na região crítica
Opera-se sobre a região crítica
Sinaliza-se a saída da região crítica
A abordagem deve garantir que
Ao entrar, somente 1 processo execute/acesse a região crítica
Ao sair, permite que outro processo entre na seção crítica
Região crítica Requisitos para solução do problema
Exclusão mútua
A somente um processo por vez é permitido entrar na seção crítica;
Progresso
Deve ser permitido a um processo entrar na sua seção crítica se nenhum outro
processo está usando a seção crítica
Espera limitada
Um processo não pode ficar indefinidamente esperando para entrar na seção
crítica, enquanto outros processos, repetidamente, entram e saem de suas
respectivas seções críticas.
Sumário
Conceituação
Princípios de concorrência
Região crítica
Coordenação de Acesso à Região crítica
Solução por Hardware
Desabilitar interrupções
Nada interrompe as instruções em operação
Instruções implementadas por hardware
test-and-set (atômicas)
Test And Set
Exclusão mútua
Arquiteturas de processador com instrução TAS (Test And Set)
Lê posição de memória e atualiza os bits de estado
Escreve na posição de memória o conteúdo de um registrador
Leitura e escrita
Realizadas atomicamente
function Test_And_Set (var lock : boolean): boolean;
begin
Test_And_Set := lock;
lock := true;
end;
Produtor Consumidorwhile (cond) {
dado = produz();
while (contador == n);
enter();
buffer[novo] = dado;
novo = (novo+1) mod n;
contador++;
leave();
}
while (cond) {
while (contador == 0);
enter();
dado = buffer[prox];
prox = (prox+1) mod n;
contador--;
leave();
consome(dado);
}
Test And Set
Exclusão mútua entre consumidor e produtor, com a instrução TAS
enter testa uma variável, lock se
lock = 1, entrada na seção crítica permitida
lock = 0, espera até que lock = 1
ENTER: MOV #0,R1
TAS R1,LOCK
JZ ENTER
RET
LEAVE: MOV #1,R1
STORE R1,LOCK
RET
Test And Set
Esta solução é chamada espera ociosa
Processo em leave mantém o processador desnecessariamente
Má utilização do tempo do processador
Solução por software
Algoritmo de Dekker
Implementação deDjikstra Propôs série de soluções evolutivas
Cada uma evidencia bugs comuns em programas concorrentes
1 - uma variável
2 - duas variáveis
3 - três variáveis
4 - correta
Algoritmo de Peterson garante
Exclusão mútua
Justiça para 2 processos
.
.
.while turn <> 0 do { nothing };
< região crítica >
turn := 1;...
Processo 0...while turn <> 1 do { nothing };
< região crítica >
turn := 0;...
Processo 1
variável global compartilhada => turn: 0..1;
Primeira Tentativa Espera ociosa(busy-waiting) => while
Alternância explícita dos processos
Velocidade ditada pelo mais lento
Problema da solução?
.
.
.while turn <> 0 do { nothing };
< região crítica >
turn := 1;...
Processo 0...while turn <> 1 do { nothing };
< região crítica >
turn := 0;...
Processo 1
variável global compartilhada => turn: 0..1;
Primeira Tentativa Espera ociosa(busy-waiting) => while
Alternância explícita dos processos
Velocidade ditada pelo mais lento
Problema da solução?
Espera ociosa e Intertravamento (um proc. pode necessitar mais acesso)
.
.
.while flag[ 1 ] do { nothing };
flag[ 0 ] := true;
< região crítica >
flag[ 0 ] := false;...
Processo 0...while flag[ 0 ] do { nothing };
flag[ 1 ] := true;
< região crítica >
flag[ 1 ] := false;...
Processo 1
variável global compartilhada => var flag: array [0..1] of boolean;
Segunda Tentativa Cada processo
Tem sua própria chave para a RC
Vê o quadro de avisos do outro mas não pode alterá-lo
Não existe bloqueio se outro processo falha fora da Região crítica
Problema da solução?
.
.
.while flag[ 1 ] do { nothing };
flag[ 0 ] := true;
< região crítica >
flag[ 0 ] := false;...
Processo 0...while flag[ 0 ] do { nothing };
flag[ 1 ] := true;
< região crítica >
flag[ 1 ] := false;...
Processo 1
variável global compartilhada => var flag: array [0..1] of boolean;
Segunda Tentativa Cada processo
Tem sua própria chave para a RC
Vê o quadro de avisos do outro mas não pode alterá-lo
Não existe bloqueio se outro processo falha fora da Região crítica
Problema da solução?
Pode não garantir exclusão mútua (flags inicializadas em false)
.
.
.flag[ 0 ] := true;
while flag[ 1 ] do { nothing };
< região crítica >
flag[ 0 ] := false;...
Processo 0...flag[ 1 ] := true;
while flag[ 0 ] do { nothing };
< região crítica >
flag[ 1 ] := false;...
Processo 1
Terceira Tentativa Em relação à segunda tentativa, apenas uma mudança no código
Ponto de inicialização da intenção de acesso à região crítica
Garante exclusão mútua
Cada processo pode ativar o valor de seu flag sem saber da condição do outro
Problema da solução?
.
.
.flag[ 0 ] := true;
while flag[ 1 ] do { nothing };
< região crítica >
flag[ 0 ] := false;...
Processo 0...flag[ 1 ] := true;
while flag[ 0 ] do { nothing };
< região crítica >
flag[ 1 ] := false;...
Processo 1
Terceira Tentativa Em relação à segunda tentativa, apenas uma mudança no código
Ponto de inicialização da intenção de acesso à região crítica
Garante exclusão mútua
Cada processo pode ativar o valor de seu flag sem saber da condição do outro
Problema da solução?
Propenso a deadlock
Quarta Tentativa
Processos indicam intenção de acesso à região crítica via flag
Problema da solução?
flag[0] =
flag[1] =FALSE
FALSE
.
flag[1] := true;
while flag[0] do
begin
flag[1] := false;
<pequena latência>;
flag[1] := true;
end;
< região crítica >;
flag[1] := false;
.
Process 1.
flag[0] := true;
while flag[1] do
begin
flag[0] := false;
<pequena latência>;
flag[0] := true;
end;
< região crítica >;
flag[0] := false;
.
Process 0
P1 sets flag [1] to true
TRUEP1 sets flag [1] to false
FALSE
PO sets flag [0] to true
TRUEP0 sets flag [0] to false FALSE
P0 sets flag [0] to true
TRUE
flag[0] := true; flag[1] := true;
flag[1] := false;
flag[0] := true;
flag[0] := false; flag[1] := false;
flag[1] := true;
while flag[1] do
flag[0] := true; flag[1] := true;flag[0] := true;
P0 checks flag [1]
P1 checks flag [0]
P1 sets flag [1] to true
TRUE
while flag[0] do
flag[0] := false;
while flag[1] do while flag[0] do
Quarta Tentativa Solução é quase correta
Gentileza mútua Possibilidade dos processos cederem a vez um para o outro indefinidamente
Pode acarretar em adiantamento indefinido (livelock)
Na prática
Situação difícil de se sustentar durante um longo tempo
Velocidade dos processos
Na teoria
Existe a probabilidade de ocorrer, logo
Invalida a proposta como solução geral do problema
var flag: array [0 .. 1] of boolean;
turn: 0 .. l;
procedure P0;
begin
repeat
flag [0] := true;
while flag [1] do
if turn = 1 then
begin
flag [O] := false;
while turn = 1 do {nothing};
flag [0] := true
end;
< região crítica >;
turn := 1;
flag [0] := false;
< restante >
forever
end;
procedure Pl;
begin
repeat
flag [1] := true;
while flag [0] do
if turn = 0 then
begin
flag [1] := false;
while turn = 0 do {nothing};
flag [1] := true
end;
< região crítica >;
turn := 0;
flag [1] := false;
< restante >
forever
end;
Begin
flag [0] := false;
flag [1] := false;
turn := 1;
parbegin
P0; P1
parend
end.
Solução Correta de Dekker
Garante exclusão mútua e justiça
Usa flags (intenção de acesso) e turn (direito de acesso)
Solução de Peterson
Proposta em 1981
Solução simples e elegante para o problema da exclusão mútua
Facilmente generalizada para o caso de n processos
A solução do algoritmo consiste em
Ao marcar a sua intenção de entrar na região crítica, o processo já
indica (para o caso de empate) que a vez é de um outro
var flag array [0 ..1] of boolean;
turn:0..1;
procedure P0;
begin
repeat
flag [0] := true;
turn := 1;
while flag [1] and turn = 1 do
{nothing};
< região crítica >;
flag [0] := false;
< restante >
forever
end;
procedure Pl;
begin
repeat
flag [1] := true;
turn := 0;
while flag [0] and turn = 0 do
{nothing};
< região crítica >;
flag [1] := false;
< restante >
forever
end; begin
flag [0] := false;
flag [1] := false;
turn := 1;
parbegin P0; P1 parend
end.
Algoritmo de Peterson
Garante exclusão mútua e justiça
Usa flags (intenção de acesso) e turn (direito de acesso)
Solução por software: Semáforo Semáforo
Semáforos são usados para exclusão mútua
Um semáforo s é uma estrutura de dados, formada por
Um contador
Um apontador para uma fila de processos bloqueados no semáforo
Somente pode ser acessado por duas operações atômicas
Se dois processos tentam acessar ou liberar uma região simultaneamente
Essas operações serão seqüencializadas
Primitivas
struct semaphore {int count;queueType queue;
}
void semWait(semaphore s){ // função Ps.count--;if(s.count<0){
1. Coloca o processo na lista de espera (s.queue)2. Bloqueia o processo
}}
void semSignal(semaphore s){ // função Vs.count++;if(s.count<=0){
1. Remove um processo P da lista de espera (s.queue)2. Coloca o processo P na lista de pronto
}}
Semáforo (Pthreads)
Fluxo de acesso à região crítica quando do emprego de semáforos
Semáforo (Pthreads)
• Permite:
• Controlar o fluxo de controle do programa e
• Acessar a áreas de dados compartilhadas por threads concorrentes
• Especificar um número determinado de threads que podem entrar na seção
crítica
• Exemplo
• Fila para pegar passagem no aeroporto com 5 caixas, quando uma caixa fica
livre outro cliente pode ser tratado, mas somente 5 clientes podem ser
tratados concorrentemente
Semáforo (Pthreads)
Semáforo (Pthreads) Principais operações
int sem_init(sem_t *sem, int pshared, unsigned int value);
Inicializa um semáforo
Parâmetros
sem: Objeto semáforo a ser inicializado
pshared: Se diferente de zero indica que o semaforo pode ser compartilhado entre processos pesados
Value: Valor de inicialização do contador do semáforo
int sem_wait(sem_t * sem);
Decrementa o valor do semáforo
Se o valor do semáforo for negativo então
O processo que chamou o comando wait é bloqueado
Senão
O processo acessa a região crítica
Semáforo (Pthreads) Principais operações int sem_trywait(sem_t * sem);
Decrementa o valor do semáforo somente se este não estiver com valor negativo
Retorna 0 (zero) se o valor do semáforo não for negativo
O processo que chamou o comando wait é bloqueado
Senão
O processo que chamou não é bloqueado
int sem_post(sem_t * sem);
Incrementa o valor do semaforo
Move um processo da lista de bloqueados para a lista de pronto, se houve
int sem_getvalue(sem_t * sem, int * sval);
Captura o valor atual do semáforo e armazena em sval
int sem_destroy(sem_t * sem);
Elimina o controle dado pelo semáforo
#include <pthread.h>#include <semaphore.h>
sem_t s0, s1;int buffer;
void main() {pthread_t tid1, tid2;sem_init(&s0, 0, 1);sem_init(&s1, 0, 0);pthread_create(&tid1, NULL, produtor, NULL);pthread_create(&tid2, NULL, consumidor, NULL);pthread_join(tid1, NULL);pthread_join(tid2, NULL);
}
void *produtor() {int i;for(i=0; i<100; i++) {sem_wait(&s0);buffer = i;sem_post(&s1);
}}
void *consumidor() {int i, k;for(i=0; i<100; i++) {sem_wait(&s1);k = buffer;sem_post(&s0);printf("Valor consumido: %d\n", k);
}}
Semáforo (Pthreads) – prod cons
Mutex (Pthreads) Utilizado para :
Proteger estruturas de dados compartilhadas em modificações concorrentes
Implementar seções críticas e monitores
Similar a um semáforo, mas que só pode assumir dois valores
Locked: Bloqueia o acesso a uma dada região crítica
Unlocked: Libera o acesso a uma dada região crítica
Cenário de operação com mutex
Uma thread que deseja fazer lock em mutex que já está com lock por outra
thread é suspenso até que a thread faça o unlock do mutex primeiro
Principais Operações int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutex-attr_t *mutexattr);
Inicializa os valores do multiplexador
Parametros:
Mutex: objeto mutex
Mutexattr:
Objeto mutex_t pode ser inicializado estaticamente na criação com o comando
pthread_mutex_t myMutex = PTHREAD_MUTEX_INITIALIZER;
int pthread_mutex_lock(pthread_mutex_t *mutex);
Impede o acesso de outros processo à região crítica.
Se região estiver bloqueada, o processo chamador é suspenso
int pthread_mutex_trylock(pthread_mutex_t *mutex);
Tenta impedir o acesso de outros processo à região crítica.
Se região já estiver bloqueada, o processo chamador não é bloqueado
Mutex (Pthreads)
Mutex (Principais Operações)
int pthread_mutex_unlock(pthread_mutex_t *mutex);
Libera o acesso à região crítica
int pthread_mutex_destroy(pthread_mutex_t *mutex);
Destroy o controle dado pelo objeto mutex
Mutex (Pthreads)
#include <stdio.h>#include <stdlib.h>#include <pthread.h>int saldo = 100;pthread_mutex_t m;
void * deposita( void *str ) {int i, a;for(i=0; i<100; i++) {
pthread_mutex_lock(&m);a = saldo; a = a + 1; saldo = a;pthread_mutex_unlock(&m);
}}
void * retira( void *str ) {int i, b;for (i=0; i<100; i++) {
pthread_mutex_lock(&m);b = saldo;b = b - 1;saldo = b;pthread_mutex_unlock(&m);
}}
void main() {pthread_t thid1, thid2;pthread_mutex_init(&m, NULL);
pthread_create (&thid1, NULL, deposita, NULL) != 0);pthread_create (&thid2, NULL, retira, NULL) != 0);
pthread_join(thid1, NULL);pthread_join(thid2, NULL);
printf(“SALDO ATUAL = %d\n”, saldo);}
Mutex (Pthreads) – Exemplo