6.1 Lições de Sistemas Operativos
Sistemas Operativos
Cap. VI
Sincronização de Threads
Prof. José Rogado
Universidade Lusófona
6.2 Lições de Sistemas Operativos
Sincronização de Threads
Noções de Concorrência
Secções Críticas
Soluções Software
Hardware de Sincronização
Semáforos
Problemas Clássicos de Sincronização
Monitores
Exemplos de Sincronização
Transacções Atómicas
6.3 Lições de Sistemas Operativos
Concorrência
Vantagens de utilizar execução concorrente
Performance
Economia
Algumas linguagens de programação suportam programação
concorrente de forma nativa (Java, C#)
Existem vários paradigmas de programação concorrente
Não existem modelos gerais
Cada modelo requer uma abordagem específica
Os suportes da concorrência a nível dos sistemas operativos
são geralmente de baixo nível
Implementados no hardware
6.4 Lições de Sistemas Operativos
Problemas
Acessos simultâneos (concorrentes) a dados partilhados
por vários fluxos de execução podem gerar
inconsistências
Para manter a consistência dos dados são necessários
mecanismos para garantir a serialização de fluxos de
execução concorrentes
Processos cooperando numa mesma aplicação
usando memória partilhada
Threads dentro de um mesmo processo utilizando as
mesmas varáveis globais
Identificação das secções críticas de código
Acessos concorrentes a dados comuns
6.5 Lições de Sistemas Operativos
Analogia: Cruzamento de Tráfego
Source: Operating Systems, Gary Nutt
Copyright © 2004 Pearson Education, Inc.
6.6 Lições de Sistemas Operativos
shared double balance;
Code for th1 Code for th2
. . . . . .
balance = balance + amount; balance = balance - amount;
. . . . . .
balance+=amount balance-=amount
balance
Source: Operating Systems, Gary Nutt
Copyright © 2004 Pearson Education, Inc.
Secção Crítica: exemplo
6.7 Lições de Sistemas Operativos
…
load R1, balance
load R2, amount
…
load R1, balance
load R2, amount
sub R1, R2
store R1, balance
…
add R1, R2
store R1, balance
…
Timer interrupt
Timer interrupt
Execution of th1 Execution of th2
Source: Operating Systems, Gary Nutt
Copyright © 2004 Pearson Education, Inc.
Secção Critica: problema
6.8 Lições de Sistemas Operativos
Secção Crítica (SC): Zona de código que só pode ser utilizada por uma
thread de cada vez, em exclusão mútua
Existe uma competição entre threads para executar a secção crítica
A secção crítica pode ter forma diferente (código) em cada thread
Não é facilmente detectavel por análise estatística
Sem exclusão mútua, o resultado da execução das threads é
indeterminado
São necessários mecanismos do SO para resolver os problemas
levantados pela concorrência
Secção Crítica: definição
Source: Operating Systems, Gary Nutt
Copyright © 2004 Pearson Education, Inc.
6.9 Lições de Sistemas Operativos
do {
entrada na secção crítica
secção crítica
saída da secção crítica
resto da thread
} while (TRUE) ;
Estrutura de uma Secção Crítica
Secção Crítica
6.10 Lições de Sistemas Operativos
Solução do Problema
1. Exclusão Mútua – Se a thread Ti está a executar uma
secção crítica, então nenhuma outra thread pode estar a
executar a mesma secção crítica (SC) em simultâneo
2. Progresso – Só as threads que competem para uma
secção crítica são elegíveis para entrar nessa secção, e a
partir do momento em que uma thread requisita o acesso à
SC este não pode ser adiado indefinidamente
3. Espera Limitada – Depois de uma thread ter requisitado o
acesso à SC, só um número limitado de outras threads
podem entrar na SC antes deste
6.11 Lições de Sistemas Operativos
Algumas Soluções Possíveis
Desactivar as interrupções
Só válido em mono-processadores
Kernel não preemptivo
Perca de performance
Utilizar soluções de Software (Peterson)
Soluções complexas válidas para poucas threads
Situações de bloqueio difíceis de evitar
Utilizar soluções de Hardware
Instruções específicas do processador
Geralmente utilizadas no kernel
Fornecidas às aplicações através de APIs de programação de
mais alto nível
Pthreads
Java
6.12 Lições de Sistemas Operativos
Desactivar as Interrupções
shared double balance;
Code for th1 Code for th2 disableInterrupts(); disableInterrupts();
balance = balance + amount; balance = balance - amount;
enableInterrupts(); enableInterrupts();
As interrupções podem ficar desactivadas durante tempos demasiados
longos
Ao sincronizar th1 e th2 está-se a impedir que outras threads possam
continuar a sua execução
Source: Operating Systems, Gary Nutt
Copyright © 2004 Pearson Education, Inc.
6.13 Lições de Sistemas Operativos
Utilização de uma Variável de Sincronização
shared boolean lock = FALSE;
shared double balance;
Code for th1 Code for th2 /* Acquire the lock */ /* Acquire the lock */
while(lock) while(lock)
; ;
lock = TRUE; lock = TRUE;
/* Execute critical sect */ /* Execute critical sect */
balance = balance + amount; balance = balance - amount;
/* Release lock */ /* Release lock */
lock = FALSE; lock = FALSE;
p1
p2
Blo
cked
at
while
lock = TRUE
lock = FALSE
Inte
rru
pt
Inte
rrupt
Inte
rrupt
Espera activa !!
Source: Operating Systems, Gary Nutt
Copyright © 2004 Pearson Education, Inc.
6.14 Lições de Sistemas Operativos
Falsa “Solução” !!
shared boolean lock = FALSE;
shared double balance;
Code for th1 Code for th2 /* Acquire the lock */ /* Acquire the lock */
while(lock) ; while(lock) ;
lock = TRUE; lock = TRUE;
/* Execute critical sect */ /* Execute critical sect */
balance = balance + amount; balance = balance - amount;
/* Release lock */ /* Release lock */
lock = FALSE; lock = FALSE;
A thread th1 … testa a variável, e encontra-a com o valor FALSE
Decide entrar na secção crítica, sai do while e é interrompido
O A thread th2 … testa a variável e encontra-a também a FALSE
Decide entrar também na secção crítica
Resultado: duas threads na mesma secção crítica…
Source: Operating Systems, Gary Nutt
Copyright © 2004 Pearson Education, Inc.
6.15 Lições de Sistemas Operativos
Sincronização Hardware
A maioria dos processadores modernos fornecem instruções específicas para implementar secções críticas
Atómicas = não interruptíveis
Dois tipos mais frequentes
Test-and-set
Teste de uma variável em memória e modificação do seu valor
Swap
Troca (swap) os valores de duas variáveis em memória
6.16 Lições de Sistemas Operativos
Instrução Test-and-Set
Definição formal:
boolean atomic TestAndSet (boolean *target)
{
boolean rv = *target;
*target = TRUE;
return rv:
}
6.17 Lições de Sistemas Operativos
Detalhe do Test and Set
FALSE m
Primary
Memory
… R3 …
Data
Register CC
Register
(a) Antes de executar TS
TRUE m
Primary
Memory
FALSE R3 =0
Data
Register CC
Register
(b) Depois de executar TS
TS(m): [Reg_i = memory[m]; memory[m] = TRUE;]
Source: Operating Systems, Gary Nutt
Copyright © 2004 Pearson Education, Inc.
6.18 Lições de Sistemas Operativos
Solução utilizando Test-and-Set
Variável booleana partilhada lock, inicializada a FALSE.
boolean lock = FALSE;
// Critical section
while (TestAndSet (&lock ))
; // wait
// Enter critical section
lock = FALSE;
// Exit critical section
6.19 Lições de Sistemas Operativos
Solução !!
shared boolean lock = FALSE;
shared double balance;
Code for th1 Code for th2 // Acquire the lock // Acquire the lock
while(TAS(&lock)) while(TAS(&lock))
; ;
// Execute critical section // Execute critical section
balance = balance + amount; balance = balance - amount;
// Release lock // Release lock
lock = FALSE; lock = FALSE;
A thread th1 testa a variável, e encontra-a com o valor FALSE
Mas a instrução TAS mudou o valor para TRUE no mesmo ciclo
A thread th2 testa a variável e encontra-a com o valor TRUE
Espera para entrar na secção crítica
Resultado: uma só thread de cada vez na secção crítica
Source: Operating Systems, Gary Nutt
Copyright © 2004 Pearson Education, Inc.
6.20 Lições de Sistemas Operativos
Problema
A espera à entrada da secção crítica é feita através de uma
espera activa
Busy wait
Embora não entre na secção crítica, a thread em espera
consome CPU e realiza acessos contínuos à memória
O Test-and-Set implica acesso à variável de sincronização
Esta solução não permite esperas longas
Solução:
Definição de objectos de sincronização mais complexos que
garantam exclusão mútua sem esperas activas
Criação de uma Hierarquia de Sincronização
6.21 Lições de Sistemas Operativos
Semáforos Source: Operating Systems, Gary Nutt
Copyright © 2004 Pearson Education, Inc.
6.22 Lições de Sistemas Operativos
Definição de Semáforo
A noção de Semáforo foi inicialmente proposta por Edsger
Dijkstra em 1968.
Um semáforo contém um valor inteiro que só pode ser
modificado através de duas operações indivisíveis (atómicas):
Originalmente chamadas P() e V()
Proberen (testar) e Verhogen (incrementar) em holandês
O nome das operações evoluíram para
wait() e signal()
Mais de acordo com as suas funcionalidades
6.23 Lições de Sistemas Operativos
Operações sobre um Semáforo
Definição de P() - wait() e V() - signal()
atomic wait (S) {
while (S <= 0)
; // wait
S--;
}
atomic signal (S) {
S++;
}
atomic wait (S) {
waitfor (S > 0);
S = S - 1;
}
6.24 Lições de Sistemas Operativos
Ferramenta de Sincronização Genérica
Um semáforo pode ser de contagem (counting) ou binário (mutex)
Counting -> sincronização de n recursos distintos
O valor inicial do semáforo é um inteiro entre 1 e N.
Mutex -> exclusão mútua de secções críticas
O valor do semáforo só pode ser 1 ou 0
O Mutex permite criar exclusão mútua em secções críticas
Semaphore S; // initialized to 1
wait (S);
Critical Section
signal (S);
6.25 Lições de Sistemas Operativos
Exemplos de Utilização
semaphore mutex = 1;
thread_create(th_0, 0);
thread_create(th_1, 0);
th_0() { th_1() {
while(TRUE) { while(TRUE {
<compute section>; <compute section>;
wait(mutex); wait(mutex);
<critical section>; <critical section>;
signal(mutex); signal(mutex);
} }
} }
Source: Operating Systems, Gary Nutt
Copyright © 2004 Pearson Education, Inc.
6.26 Lições de Sistemas Operativos
Problema do Balanço de Conta
semaphore mutex = 1;
shared int balance;
thread_create(th_0, 0);
thread_create(th_1, 0);
th_0() { th_1() {
. . . . . .
/* Enter the CS */ /* Enter the CS */
wait(mutex); wait(mutex);
balance += amount; balance -= amount;
signal(mutex); signal(mutex);
. . . . . .
} }
Source: Operating Systems, Gary Nutt
Copyright © 2004 Pearson Education, Inc.
6.27 Lições de Sistemas Operativos
Implementação de Semáforos
É preciso garantir que não há mais de uma thread a executar wait() e
signal() no mesmo semáforo ao mesmo tempo
A implementação reduz-se ao problema da criar uma secção crítica
para proteger o código internos das funções wait() e signal()
Um semáforo binário pode ser implementado simplesmente com uma
instrução Test-and-Set e uma variável binária
O valor FALSE indica que o semáforo está livre e TRUE ocupado
Mas a espera não pode ser feita desta forma!
boolean s = FALSE;
// Wait definition
wait(s) {
while(TS(s))
; // wait
}
// Signal Definition
signal(s) {
s = FALSE;
}
6.28 Lições de Sistemas Operativos
Espera Activa
Numa implementação básica de semáforos, a operação wait()
implicaria uma espera activa pelo valor positivo do semáforo
Busy wait ou Spin lock
A thread testa o valor e caso este seja menor ou igual a zero, volta
a testar, continuando a concorrer com os outras threads
Está a consumir recursos quando não tem condições para se
executar!!
atomic wait (S) {
while (S <= 0)
; // busy wait
S = S - 1;
}
6.29 Lições de Sistemas Operativos
Semáforos sem Espera Activa
Uma solução mais correcta é pôr a thread em espera através de invocações explícitas do Scheduler
wait(): retira a thread da Ready Queue no caso do semáforo estar ocupado
signal(): se houver threads à espera do semáforo, transfere pelo menos um para a Ready Queue (RQ)
Assim a espera num semáforo assemelha-se a qualquer outra operação de espera por recursos (p.ex.: I/O)
Ao semáforo é associada uma fila de espera de threads e duas rotinas:
sleep() – retira o processador à thread corrente e coloca-a na em estado waiting fila de espera associada ao semáforo.
wakeup() – retira pelo menos uma thread da fila de espera do semáforo e volta a inseri-la em estado active na Ready Queue.
A nova thread a executar depende dos critérios de scheduling do sistema
6.30 Lições de Sistemas Operativos
Implementação do Semáforo Genérico
A implementação de um semáforo com contador é mais complexa
e necessita de uma estrutura com três campos:
O campo mutex permite criar e proteger a secção crítica
correspondente à modificação do valor do semáforo
O campo value contém o valor do semáforo (nº de recursos livres)
que é negativo no caso de haver threads em espera
O campo queue permite colocar a thread numa fila de espera no
caso do valor do semáforo ser <= 0
Todos os campos do semáforo são modificados em exclusão
mútua através de instruções Test-and-Set sobre o mutex
Um semáforo binário é um semáforo genérico com valor inicial = 1
struct semaphore {
boolean mutex = FALSE;
int value = <initial value>;
struct thread *queue = NULL;
};
6.31 Lições de Sistemas Operativos
Implementação com Fila de Espera
struct semaphore {
struct thread *queue;
int value = <initial value>;
boolean mutex = FALSE;
};
shared struct semaphore s;
wait(struct semaphore s){
while(TS(s.mutex))
; // wait
s.value--;
if(s.value < 0) {
s.mutex = FALSE;
sleep(s.queue);
} else
s.mutex = FALSE;
}
signal(struct semaphore s){
while(TS(s.mutex))
; // wait
s.value++;
if(s.value <= 0) {
while(s.queue == NULL)
; // wait
wakeup(s.queue);
}
s.mutex = FALSE;
}
So
urc
e:
Op
era
tin
g S
ys
tem
s,
Ga
ry N
utt
Co
pyri
gh
t ©
20
04
Pe
ars
on
Ed
uc
ati
on
, In
c.
6.32 Lições de Sistemas Operativos
Implementação com Fila de Espera
struct semaphore {
struct thread *queue;
int value = <initial value>;
boolean mutex = FALSE;
};
shared struct semaphore s;
wait(struct semaphore s){
while(TS(s.mutex))
; // wait
s.value--;
if(s.value < 0) {
s.mutex = FALSE;
sleep(s.queue);
} else
s.mutex = FALSE;
}
signal(struct semaphore s){
while(TS(s.mutex))
; // wait
s.value++;
if(s.value <= 0) {
while(s.queue == NULL)
; // wait
wakeup(s.queue);
}
s.mutex = FALSE;
}
So
urc
e:
Op
era
tin
g S
ys
tem
s,
Ga
ry N
utt
Co
pyri
gh
t ©
20
04
Pe
ars
on
Ed
uc
ati
on
, In
c.
O wakeup é feito pela primitiva
signal() sempre s.value <= 0 ou
seja, sempre que haja threads
em espera
Neste caso o recurso é
“transferido” da thread que o
liberta para uma das threads
que está à espera em sleep()
6.33 Lições de Sistemas Operativos
Exemplo
Semáforo
Th 1 Th 2
6.34 Lições de Sistemas Operativos
Exemplo
Semáforo
Th 1 Th 2
Th 2
6.35 Lições de Sistemas Operativos
Exemplo
Semáforo
Th 1 Th 2
6.36 Lições de Sistemas Operativos
Deadlock e Starvation
Deadlock – duas ou mais threads esperam para sempre por um evento que só uma das threads bloqueados pode produzir
Se S e Q forem dois semáforos inicializados a 1, a ordem de invocação apresentada conduz a um deadlock
T0 T1
wait (S); wait (Q);
wait (Q); wait (S);
. .
. .
. .
signal (Q); signal (S);
signal (S); signal (Q);
Starvation – bloqueamento indefinido: uma thread nunca mais é removida da fila de espera em que está colocada, por ser removida numa ordem diferente da que foi colocada
1 2
3 4
Deadlock !
6.37 Lições de Sistemas Operativos
Problemas Clássicos de Sincronização
São problemas que retratam de forma simplificada a essência de
alguns problemas de sincronização existentes em situações reais
Partilha de Dados
Problema do Buffer Limitado
Problema dos Leitores e Escritores
Problema dos Filósofos à Mesa
6.38 Lições de Sistemas Operativos
Sincronização: Partilha de 2 Variáveis
Problema:
Dois processos P1 e P2 trabalham em cooperação
Utilizam duas variáveis partilhadas x e y para trocar valores
Os processos têm de se sincronizar para que os valores das variáveis sejam significativos
Algoritmo:
P1 actualiza x e continua a execução
P2 deve ler o valor posto em x por P1 e actualizar y
P1 deve esperar que P2 actualize y e ler o seu valor
Sincronização
Dois semáforos S1 e S2 associados a x e y
Os semáforos servem de sincronização entre P1 e P2
Podem também criar um deadlock se forem mal geridos
6.39 Lições de Sistemas Operativos
Partilha de 2 Variáveis (Cont.)
proc_A() {
while(TRUE) {
<compute section A1>;
update(x);
/* Signal proc_B */
signal(s1);
<compute section A2>;
/* Wait for proc_B */
wait(s2);
retrieve(y);
}
}
proc_B() {
while(TRUE) {
/* Wait for proc_A */
wait(s1);
retrieve(x);
<compute section B1>;
update(y);
/* Signal proc_A */
signal(s2);
<compute section B2>;
}
}
semaphore s1 = 0; // Protects x, initially wait
semaphore s2 = 0; // Protects y, initially wait
shared int x, y;
create_thread(proc_A, 0);
create_thread(proc_B, 0);
So
urc
e:
Op
era
tin
g S
ys
tem
s,
Ga
ry N
utt
Co
pyri
gh
t ©
20
04
Pe
ars
on
Ed
uc
ati
on
, In
c.
6.40 Lições de Sistemas Operativos
Problema do Buffer Limitado
Empty Pool
Full Pool
Um processo produz dados que outro consome
Utilizam uma pool de buffers para passar dados de um para o outro
O produtor pode funcionar enquanto houver buffers vazios, e espera quando estiverem todos cheios, até que um esteja vazio
O consumidor pode funcionar enquanto houver buffers cheios e espera quando estiverem todos vazios, até que um esteja cheio
Producer Consumer
Data in Data out
So
urc
e:
Op
era
tin
g S
ys
tem
s,
Ga
ry N
utt
Co
pyri
gh
t ©
20
04
Pe
ars
on
Ed
uc
ati
on
, In
c.
6.41 Lições de Sistemas Operativos
Problema do Buffer Limitado (2)
semaphore mutex = 1;
semaphore full = 0; /* A general (counting) semaphore */
semaphore empty = N; /* A general (counting) semaphore */
buf_type buffer[N];
create_thread(producer);
create_thread(consumer);
producer() {
buf_type *data_in, *buffer;
while(TRUE) {
produce_item(data_in);
/* Get an empty buffer */
wait(empty);
wait(mutex);
buffer = get(emptyPool);
signal(mutex);
copy_buffer(data_in, buffer);
wait(mutex);
release(buffer, fullPool);
signal(mutex);
/* Signal a full buffer */
signal(full);
}
}
consumer() {
buf_type *data_out, *buffer;
while(TRUE) {
/* Get a full buffer */
wait(full);
wait(mutex);
buffer = get(fullPool);
signal(mutex);
copy_buffer(buffer, data_out);
wait(mutex);
release(buffer, emptyPool);
signal(mutex);
/* Signal an empty buffer */
signal(empty);
consume_item(data_out);
}
}
So
urc
e:
Op
era
tin
g S
ys
tem
s,
Ga
ry N
utt
Co
pyri
gh
t ©
20
04
Pe
ars
on
Ed
uc
ati
on
, In
c.
6.42 Lições de Sistemas Operativos
Problema do Buffer Limitado (2)
semaphore mutex_empty = 1, mutex_full = 1;
semaphore full = 0; /* A general (counting) semaphore */
semaphore empty = N; /* A general (counting) semaphore */
buf_type buffer[N];
create_thread(producer);
create_thread(consumer);
producer() {
buf_type *data_in, *buffer;
while(TRUE) {
produce_item(data_in);
/* Get an empty buffer */
wait(empty);
wait(mutex_empty);
buffer = get(emptyPool);
signal(mutex_empty);
copy_buffer(data_in, buffer);
wait(mutex_full);
release(buffer, fullPool);
signal(mutex_full);
/* Signal a full buffer */
signal(full);
}
}
consumer() {
buf_type *data_out, *buffer;
while(TRUE) {
/* Get a full buffer */
wait(full);
wait(mutex_full);
buffer = get(fullPool);
signal(mutex_full);
copy_buffer(buffer, data_out);
wait(mutex_empty);
release(buffer, emptyPool);
signal(mutex_empty);
/* Signal an empty buffer */
signal(empty);
consume_item(data_out);
}
}
So
urc
e:
Op
era
tin
g S
ys
tem
s,
Ga
ry N
utt
Co
pyri
gh
t ©
20
04
Pe
ars
on
Ed
uc
ati
on
, In
c.
Um mutex por cada pool: melhor
6.43 Lições de Sistemas Operativos
Problema dos Leitores e Escritores
Leitores
Escritores
So
urc
e:
Op
era
tin
g S
ys
tem
s,
Ga
ry N
utt
Co
pyri
gh
t ©
20
04
Pe
ars
on
Ed
uc
ati
on
, In
c.
6.44 Lições de Sistemas Operativos
Problema dos Leitores e Escritores (2)
Reader Reader
Reader Reader
Reader Reader Reader Reader
Writer Writer Writer Writer Writer Writer Writer
So
urc
e:
Op
era
tin
g S
ys
tem
s,
Ga
ry N
utt
Co
pyri
gh
t ©
20
04
Pe
ars
on
Ed
uc
ati
on
, In
c.
Um conjunto de processos realizam acessos a um recurso comum,
uns para escrever, outros para ler o seu conteúdo
Ficheiro, BD, …
É necessário garantir a coerência dos valores
Shared
Resource
6.45 Lições de Sistemas Operativos
Problema dos Leitores e Escritores (3)
Reader Reader Reader
Reader Reader Reader
Reader Reader
Writer Writer Writer
Writer Writer Writer Writer
So
urc
e:
Op
era
tin
g S
ys
tem
s,
Ga
ry N
utt
Co
pyri
gh
t ©
20
04
Pe
ars
on
Ed
uc
ati
on
, In
c.
N leitores podem-se executar simultaneamente
Shared
Resource
6.46 Lições de Sistemas Operativos
Problema dos Leitores e Escritores (4)
Reader
Shared
Resource
Reader Reader Reader Reader
Reader Reader Reader
Writer Writer Writer Writer Writer Writer
Writer
So
urc
e:
Op
era
tin
g S
ys
tem
s,
Ga
ry N
utt
Co
pyri
gh
t ©
20
04
Pe
ars
on
Ed
uc
ati
on
, In
c.
Só um escritor se pode executar de cada vez
Quando um escritor está activo, os leitores esperam
Quando há leitores activos, o escritor espera
6.47 Lições de Sistemas Operativos
Primeira Solução resourceType *resource;
int readCount = 0;
semaphore mutex = 1;
semaphore writeBlock = 1;
create_thread(reader, 0);
create_thread(writer, 0);
reader() {
while(TRUE) {
<other computing>;
wait(mutex);
readCount++;
if(readCount == 1)
wait(writeBlock);
signal(mutex);
read(resource);
/* Critical section */
wait(mutex);
readCount--;
if(readCount == 0)
signal(writeBlock);
signal(mutex);
}
}
writer() {
while(TRUE) {
<other computing>;
/* Critical section */
wait(writeBlock);
write(resource);
signal(writeBlock);
}
}
So
urc
e:
Op
era
tin
g S
ys
tem
s,
Ga
ry N
utt
Co
pyri
gh
t ©
20
04
Pe
ars
on
Ed
uc
ati
on
, In
c.
O primeiro leitor bloqueia os escritores
O primeiro escritor bloqueia os leitores
O último leitor desbloqueia os escritores
Qualquer escritor espera por todos os
leitores
Os leitores podem causar starvation dos
escritores porque as actualizações “não
passam”
6.48 Lições de Sistemas Operativos
Segunda Solução
O algoritmo é modificado para privilegiar os escritores
Os leitores continuam a poder ler sem exclusão mútua
Se aparece um escritor e está um leitor activo, o escritor
espera
Se um leitor aparece com um escritor em espera, o leitor
espera que o escritor saia
Se um segundo escritor aparece, tem prioridade sobre os
leitores que podem esperar indefinidamente
Esta solução é complexa e necessita uma implementação
cautelosa para evitar deadlocks e starvation
Veremos uma solução mais robusta baseada em monitores
6.49 Lições de Sistemas Operativos
Prioridade aos Escritores int readCount = 0, writeCount = 0;
semaphore mutex1 = 1, mutex2 = 1;
semaphore readBlock = 1, writeBlock = 1;
create_thread(reader, 0);
create_thread(writer, 0);
reader() {
while(TRUE) {
<other computing>;
wait(readBlock);
wait(mutex1);
readCount++;
if(readCount == 1)
wait(writeBlock);
signal(mutex1);
signal(readBlock);
read(resource);
wait(mutex1);
readCount--;
if(readCount == 0)
signal(writeBlock);
signal(mutex1);
}
}
writer() {
while(TRUE) {
<other computing>;
wait(mutex2);
writeCount++;
if(writeCount == 1)
wait(readBlock);
signal(mutex2);
wait(writeBlock);
write(resource);
signal(writeBlock);
wait(mutex2)
writeCount--;
if(writeCount == 0)
signal(readBlock);
signal(mutex2);
}
}
1 2
3
4
5
So
urc
e:
Op
era
tin
g S
ys
tem
s,
Ga
ry N
utt
Co
pyri
gh
t ©
20
04
Pe
ars
on
Ed
uc
ati
on
, In
c.
6.50 Lições de Sistemas Operativos
Problema dos Filósofos à Mesa
Cinco filósofos estão sentados a uma
mesa a comer massa:
Cada um tem um talher de cada lado,
que partilha com o seu vizinho do
lado
Cada filósofo só pode comer quando
ficam livres simultaneamente os dois
talheres de cada lado do seu prato
Dois filósofos sentados ao lado um do
outro não podem comer ao mesmo
tempo.
So
urc
e:
Op
era
tin
g S
ys
tem
s,
Ga
ry N
utt
Co
pyri
gh
t ©
20
04
Pe
ars
on
Ed
uc
ati
on
, In
c.
6.51 Lições de Sistemas Operativos
Problema dos Filósofos à Mesa
Para resolver o problema vamos atribuir um índice a cada
filósofo, com i variando de 0 a 4
A cada garfo associa-se um semáforo designado por garfo(i)
O filósofo i pode comer quando estão livres simultaneamente os
semáforos garfo (i) e garfo ((i+1) % 5), ou seja:
garfo(0) e garfo(1)
garfo(1) e garfo(2)
garfo(2) e garfo(3)
garfo(3) e garfo(4)
garfo(4) e garfo(0)
6.52 Lições de Sistemas Operativos
Primeira Solução
semaphore garfo[5] = (1,1,1,1,1);
fork (filosofo, 1, 0);
fork (filosofo, 1, 1);
fork (filosofo, 1, 2);
fork (filosofo, 1, 3);
fork (filosofo, 1, 4);
filosofo (int i) {
while(TRUE) {
// Think
// Eat
wait(garfo[i]);
wait(garfo[(i+1) % 5]);
eat();
signal(garfo[(i+1) % 5]);
signal(garfo[i]);
}
}
So
urc
e:
Op
era
tin
g S
ys
tem
s,
Ga
ry N
utt
Co
pyri
gh
t ©
20
04
Pe
ars
on
Ed
uc
ati
on
, In
c.
garfo(i)
garfo(i+1)
filosofo(i)
6.53 Lições de Sistemas Operativos
Primeira Solução
semaphore garfo[5] = (1,1,1,1,1);
fork (filosofo, 1, 0);
fork (filosofo, 1, 1);
fork (filosofo, 1, 2);
fork (filosofo, 1, 3);
fork (filosofo, 1, 4);
filosofo (int i) {
while(TRUE) {
// Think
// Eat
wait(garfo[i]);
wait(garfo[(i+1) % 5]);
eat();
signal(garfo[(i+1) % 5]);
signal(garfo[i]);
}
}
So
urc
e:
Op
era
tin
g S
ys
tem
s,
Ga
ry N
utt
Co
pyri
gh
t ©
20
04
Pe
ars
on
Ed
uc
ati
on
, In
c.
Se todos os filósofos acederem simultaneamente aos talheres
do mesmo lado (esquerdo ou direito), cria-se uma solução de
Deadlock
6.54 Lições de Sistemas Operativos
Segunda Solução
semaphore garfo[5] = (1,1,1,1,1);
fork (filosofo, 1, 0);
fork (filosofo, 1, 1);
fork (filosofo, 1, 2);
fork (filosofo, 1, 3);
fork (filosofo, 1, 4);
filosofo (int i) {
while(TRUE) {
// Think
// Eat
j = i % 2;
wait(garfo[(i+j) % 5]);
wait(garfo[(i+1-j) % 5]);
eat();
signal(garfo[(i+1-j) % 5]);
signal(garfo[[(i+j) % 5]);
}
}
So
urc
e:
Op
era
tin
g S
ys
tem
s,
Ga
ry N
utt
Co
pyri
gh
t ©
20
04
Pe
ars
on
Ed
uc
ati
on
, In
c.
Na primeira escolha, os filósofos de índice par acedem ao garfo
esquerdo, e os de índice ímpar, o garfo direito
Na segunda escolha inverte-se a ordem
6.55 Lições de Sistemas Operativos
Segunda Solução
semaphore garfo[5] = (1,1,1,1,1);
fork (filosofo, 1, 0);
fork (filosofo, 1, 1);
fork (filosofo, 1, 2);
fork (filosofo, 1, 3);
fork (filosofo, 1, 4);
filosofo (int i) {
while(TRUE) {
// Think
// Eat
j = i % 2;
wait(garfo[(i+j) % 5]);
wait(garfo[(i+1-j) % 5]);
eat();
signal(garfo[(i+1-j) % 5]);
signal(garfo[[(i+j) % 5]);
}
}
So
urc
e:
Op
era
tin
g S
ys
tem
s,
Ga
ry N
utt
Co
pyri
gh
t ©
20
04
Pe
ars
on
Ed
uc
ati
on
, In
c.
garfo(0)
filosofo(1)
garfo(2)
filosofo(0)
filosofo(2)
filosofo(3)
filosofo(4)
garfo(3)
garfo(4) garfo(1)
Na primeira escolha, os filósofos de índice par acedem ao garfo
esquerdo, e os de índice ímpar, o garfo direito
Na segunda escolha inverte-se a ordem
6.56 Lições de Sistemas Operativos
Problemas de Utilização dos Semáforos
As operações sobre os semáforos têm de ser usadas correctamente
Se há troca de ordem, pode haver vários processos na secção crítica
wait (mutex)
// Secção Crítica
wait (mutex)
Se houver troca de funções, gera-se um deadlock
signal (mutex)
// Secção Crítica
wait (mutex)
Se houver omissão de uma das funções, um dos dois casos anteriores
pode acontecer
É relativamente frequente em programas complexos acontecerem
situações deste tipo que não são fáceis de detectar
Para evitar estas situações, algumas linguagens utilizam ferramentas
de sincronização mais fáceis de utilizar
6.57 Lições de Sistemas Operativos
Monitores Uma extensão do conceito de semáforo de mais alto nível que fornece
uma interface eficaz e fácil de utilizar para sincronização de threads ou processos
Conceito de programação Orientada aos Objectos
Embebida nas linguagens Concurrent Pascal, Java e C#
Gerida pelo compilador
O monitor encapsula (esconde) a sua implementação
Só são acessíveis métodos e dados públicos
Os métodos e dados privados são só utilizados pelo monitor
O monitor garante exclusão mútua dos processos que o executam:
monitor mon {
private:
semaphore mutex = 1; // Implícito
. . .
public:
method_i(…) {
wait(mutex); // Implícito
<code for proc_i>;
signal(mutex); // Implícito
};
};
6.58 Lições de Sistemas Operativos
Exemplo: Shared Balance
monitor sharedBalance {
private:
double balance;
public:
credit(double amount){
balance += amount;
}
debit(double amount){
balance -= amount;
}
. . .
}
O problema da partilha da variável balance é resolvido através da declaração
de um monitor contendo:
Uma variável privada balance
Dois métodos públicos que são executados no interior do monitor com
exclusão mútua implícita
6.59 Lições de Sistemas Operativos
Balanço de Conta c/ Monitores
monitor sharedBalance;
thread_create(deposit);
thread_create(withdraw);
deposit() {
. . .
sharedBalance.credit(amount);
. . .
}
withdraw() {
. . .
sharedBalance.debit(amount);
. . .
}
Os métodos deposit() e withdraw()
são executados no interior do monitor,
que garante a exclusão mútua da
secção crítica através de uma variável
de sincronização implícita
6.60 Lições de Sistemas Operativos
Representação de um Monitor
6.61 Lições de Sistemas Operativos
Variáveis de Condição
Os monitores fornecem também mecanismos que permitem às threads esperar que se verifiquem condições específicas
Designam-se por Variáveis de Condição
Geralmente, são definidas duas operações sobre uma VC x
condition_wait (x) – permite a uma thread que invoca esta operação ser suspensa até a condição se verificar
condition_signal (x) – permite a uma thread sinalizar o facto de que a condição se verificou, e activar uma das threads que estiverem em espera
Não é geralmente garantido que a thread activada seja a que mais tempo tenha esperado
A forma como as threads são reactivadas por condition_signal() depende da implementação do monitor e do algoritmo de scheduling
6.62 Lições de Sistemas Operativos
Um Monitor com Variáveis de Condição
6.63 Lições de Sistemas Operativos
Estrutura de um Monitor
Variáveis de Sincronização do Monitor
Um mutex para exclusão mútua no monitor, inicializado a 1
Um mutex de sinalização no monitor, inicializado a 0
Um contador de processos em espera
semaphore mutex = 1; // monitor mutex, initially = 1
semaphore other = 0; // signaling mutex, initially = 0
int others_waiting = 0; // number waiting for signaling
Variáveis de Condição do Monitor: para cada condição é
introduzida uma variável com a seguinte estrutura
Um semáforo de sinalização inicializado a 0
Um contador de processos em espera na variável
struct condvar x {
semaphore sem; // initially = 0
int count = 0; // waiting on x
};
6.66 Lições de Sistemas Operativos
Readers & Writers com Monitores
monitor readerWriter {
int numberOfReaders = 0;
boolean busy = FALSE;
condition okToRead, okToWrite;
public:
startRead() {
if(busy || (okToWrite.queue())
okToRead.wait();
numberOfReaders++;
okToRead.signal();
}
finishRead() {
numberOfReaders--;
if(numberOfReaders == 0)
okToWrite.signal();
}
startWrite() {
if((numberOfReaders != 0)
|| busy)
okToWrite.wait();
busy = TRUE;
}
finishWrite() {
busy = FALSE;
if(okToRead.queue())
okToRead.signal()
else
okToWrite.signal()
}
}
So
urc
e:
Op
era
tin
g S
ys
tem
s,
Ga
ry N
utt
Co
pyri
gh
t ©
20
04
Pe
ars
on
Ed
uc
ati
on
, In
c.
6.67 Lições de Sistemas Operativos
Hierarquia de Sincronização
Test -and-Set
Spin-lock
Mutex
Semáforo
Monitor Os vários objectos que
descrevemos formam uma
hierarquia de sincronização
Baseada em suporte
hardware
Cada nível utiliza as
funcionalidades do anterior
e estende-a com
capacidades adicionais
Permite garantir a vários
níveis de implementação a
correcção de aplicações
concorrentes
6.68 Lições de Sistemas Operativos
Exemplos de Sincronização
Windows
POSIX
Linux
6.69 Lições de Sistemas Operativos
Sincronização em Windows
São fornecidos dois mecanismos para exclusão mútua
Mutexes
Critical Sections
Os mutexes são geridos pelo sistema operativo através das
primitivas
HANDLE CreateMutex(attributes, Owner, Name)
WaitforSingleObject(HANDLE, timeout)
ReleaseMutex(HANDLE)
As CriticalSections secções críticas permitem criar zonas de
código protegido sem ter explicitamente de manipular mutexes
InitializeCriticalSection(…)
EnterCriticalSection(…)
LeaveCriticalSection(…)
6.70 Lições de Sistemas Operativos
Sincronização POSIX
A sincronização POSIX permite gerir exclusão mútua em
ambientes multiprogramados utilizando a API das pthreads
A API é independente do Sistema Operativo e fornece:
mutex locks
condition variables
Extensões não dependentes do SO fornecem também:
read-write locks
spin locks
6.71 Lições de Sistemas Operativos
API POSIX para Sincronização
Gestão de Mutexes
pthread_mutex_init (mutex,attr)
pthread_mutex_lock (mutex)
pthread_mutex_trylock (mutex)
pthread_mutex_unlock (mutex)
Gestão de Condition Variables
pthread_cond_init (condition,attr)
pthread_cond_wait (condition,mutex)
pthread_cond_signal (condition)
pthread_cond_broadcast (condition)
Utilização
pthread_cond_wait deve ser invocada com um mutex locked, a função liberta-o implicitamente
pthread_cond_signal deve ser invocada com um mutex locked que é necessário depois libertar para que uma thread que está em espera possa ser reactivada
6.72 Lições de Sistemas Operativos
Sincronização no Kernel Linux
Utiliza spinlocks baseados em TAS em ambiente multiprocessador
O kernel é preemptível desde a versão 2.6, mas quando um
processo kernel obtém um lock, esta possibilidade é suspensa
através da manutenção de um contador do número de
preempções em curso (preempt_count) por processo kernel
Quando as secções críticas de código são extensas, utilizam-se
semáforos
System call futex() “Fast Userspace muTexes” permite a
implementação de mutex e semáforos em modo utilizador com
recurso mínimo a funções do kernel
http://lxr.linux.no/linux/kernel/futex.c
6.73 Lições de Sistemas Operativos
Exemplo 1: API POSIX
Uma thread produtora recebe dados de um canal de I/O
Para os armazenar, retira um buffer da lista dos buffers vazios,
preenche-o com os dados recebidos e insere-o na lista dos buffers
cheios
Uma outra thread consumidora retira um buffer da lista dos buffers
cheios, envia os dados para tratamento e volta a colocar esse
buffer na lista dos buffers vazios
A thread produtora espera que haja buffers vazios
A thread consumidora espera que haja buffers cheios
Inicialmente os buffers estão todos vazios
Ver ficheiro http://netlab.ulusofona.pt/so/praticas/bound-buffer.c
6.74 Lições de Sistemas Operativos
Exemplo 2: API POSIX
Uma thread insere processos em N filas de espera ordenando-os
por prioridade
Uma outra thread retira-os por ordem de prioridade decrescente
Duas actividades concorrentes que criam uma secção crítica ao
poderem modificar simultaneamente as filas de espera
Solução: utilização de um mutex para proteger a secção crítica
Por outro lado, a remoção de processos deve ser comandada por
um evento (sinal) que activa a thread
Solução: utilização de uma variável de condição na qual a thread
de selecção vai esperar
Ver ficheiro http://netlab.ulusofona.pt/so/praticas/proc-sched-sig.c
6.75 Lições de Sistemas Operativos
Fim do Capítulo 6