[14]. Por que precisamos de mecanismos para controlar a execução de threads?
Passo 1
O enunciado pergunta claramente: “Por que precisamos de mecanismos para controlar a execução de threads?”
Informações dadas pelo enunciado: nenhuma adicional — é uma pergunta conceitual.
O que é pedido: explicar, de forma didática, os problemas que surgem quando várias threads executam concorrente e por que precisamos de mecanismos (locks, semáforos, etc.) para controlar essa execução.
Informações dadas pelo enunciado: nenhuma adicional — é uma pergunta conceitual.
O que é pedido: explicar, de forma didática, os problemas que surgem quando várias threads executam concorrente e por que precisamos de mecanismos (locks, semáforos, etc.) para controlar essa execução.
Passo 2
O que é uma thread (explicação bem leve):
- Uma thread é uma “linha de execução” dentro de um programa — pense nela como uma pessoa que executa instruções num mesmo apartamento (o processo).
- Várias threads dentro do mesmo processo compartilham memória e recursos (variáveis, estruturas, arquivos).
Isso gera concorrência: várias threads podem tentar acessar/alterar a mesma coisa ao mesmo tempo. Quando isso acontece sem controle, aparecem problemas — é por isso que precisamos de mecanismos de controle.
Passo 3
Exemplo concreto e simples (pra sentir o problema): incremento de contador.
Considere uma variável compartilhada inicializada com
. Duas threads querem executar "incrementar
em
". O código clássico (não-atomico) é:
Considere uma variável compartilhada
- Thread A: read
; ; write . - Thread B: read
; ; write .
Sequência possível (interleaving) que causa erro:
- A lê
(obtém ). - B lê
(obtém ). - A escreve
. - B escreve
.
Resultado final:
, embora duas threads tenham incrementado — deveria ser
. Esse é um exemplo de condição de corrida (race condition): o resultado depende da ordem das operações. É exatamente por isso que precisamos de mecanismos que controlem execução.
Passo 4
Conceito de seção crítica e exclusão mútua (a teoria prática para a prova):
- Seção crítica: trecho do código que acessa/modifica recurso compartilhado (por exemplo, a atualização de
). - Exclusão mútua: propriedade que garante que, a qualquer instante, no máximo uma thread esteja executando a seção crítica.
Mecanismo simples: mutex (lock). Esquema: - Antes de tocar em
: lock(mutex); - Faz a operação (seção crítica);
- Após: unlock(mutex).
Com isso, a sequência de A e B não pode se intercalar dentro da seção crítica, evitando o passo-a-passo que causou a perda de atualização.
Passo 5
Outros problemas além de race conditions e os mecanismos correspondentes (com exemplos rápidos e intuitivos):
- Sincronização por ordem/coordenação: às vezes B só pode executar depois que A produziu algo (ex.: produtor/consumidor). Solução: condition variables, semáforos, ou filas bloqueantes. Exemplo de semáforo simples: um buffer com tamanho
usa dois semáforos: empty(inicia com) e full(inicia com); produtor faz wait(empty); put(item); signal(full);, consumidor fazwait(full); get(item); signal(empty);. Assim produtores e consumidores ficam corretamente sincronizados. - Espera por término: quando uma thread precisa esperar outra terminar, usa-se
join(faz a ordenação simples: A espera B terminar). - Barreira: várias threads precisam chegar a um ponto antes de prosseguir; usa-se barrier.
Passo 6
Deadlock e starvation — por que são perigosos e como evitá-los (fatos importantes para a prova):
- Deadlock: situação em que duas ou mais threads ficam bloqueadas esperando recursos umas das outras. Condições clássicas para deadlock (Coffman): mutua exclusão, hold-and-wait, não-preempção, espera circular.
- Starvation: uma thread nunca consegue executar porque outras sempre tomam os recursos.
Prevenção/mitigação:- impor ordem fixa de aquisição de locks (evita espera circular);
- evitar manter muitos locks por longos períodos;
- usar timeouts ao adquirir locks;
- usar algoritmos que evitem hold-and-wait (adquirir todos locks de uma vez, quando possível).
Na prova: identifique locks, desenhe grafo de espera (quem espera quem) — se houver ciclo, há risco de deadlock.
Passo 7
Visibilidade de memória e operações atômicas (um problema menos óbvio, mas importante):
- Além de ordenação, caches e otimizações do compilador podem fazer com que uma escrita feita por uma thread não seja imediatamente “vista” por outra. Por isso existem primitivos chamados atomics e memory fences.
- Exemplos de operações atômicas: incremento atômico,
compare-and-swap (CAS). Com atomics, a atualização acontece como uma única operação indivisível, sem precisar de lock em alguns casos (lock-free).
Na prova: saiba distinguir entre “operacionalmente atômico” (CPU/ISA oferece) e trecho protegido por lock; mencione que variáveis marcadas como atômicas/volatile (dependendo da linguagem) têm garantias de visibilidade/ordenação.
Passo 8
Trade-offs práticos e de projeto (o que considerar na hora da prova ou exercício):
- Bloqueio (blocking) vs busy-wait (spinning): spinlocks podem ser rápidos se espera curta, mas consomem CPU; bloqueio libera CPU mas tem custo de mudança de contexto.
- Granularidade do lock: lock grande (coarse-grained) é simples mas reduz paralelismo; locks finos (fine-grained) aumentam paralelismo mas aumentam complexidade e risco de deadlock.
- Preferir estruturas de mais alto nível quando possível (filas concorrentes, monitores, bibliotecas), pois reduzem erros.
Passo 9
Como responder a uma pergunta de prova sobre isso (passo-a-passo aplicável em exame):
- Identifique quais dados/recursos são compartilhados.
- Identifique os trechos que modificam esses recursos (seções críticas).
- Mostre um exemplo de interleaving que causa erro (como o contador
acima). Isso prova que há problema. - Proponha um mecanismo concreto (mutex, semáforo, cond var, atomic) e mostre onde colocar
lock/unlockouwait/signal. Escreva o pseudocódigo protegido. - Analise efeitos colaterais: risco de deadlock? starvation? custo de desempenho? proponha mitigação.
- Se pedir, mostre alternativa lock-free com operação atômica (ex.:
atomic_fetch_add(&x,1)), e discuta vantagens/desvantagens.
Passo 10
Resumo prático e motivacional (curto e objetivo):
- Precisamos desses mecanismos porque múltiplas threads acessando o mesmo recurso sem coordenação causam resultados errôneos e não-determinísticos (condições de corrida), podem travar o sistema (deadlock), ou deixar threads sem progresso (starvation), além de problemas de visibilidade de memória.
- Mecanismos como mutexes, semáforos, condition variables, atomics e barriers existem para garantir exclusão mútua, sincronização, ordenação e visibilidade — ferramentas essenciais para construir programas concorrentes corretos e previsíveis.
Vai dar certo — na prova, siga o roteiro do Passo 9: identifique, exemplifique, proponha a correção e analise consequências.
Resposta
Porque sem mecanismos de controle threads concorrentes podem causar condições de corrida (resultados incorretos e não determinísticos), violar invariantes de dados, provocar deadlocks ou starvation e ter problemas de visibilidade de memória; mecanismos como mutexes, semáforos, condition variables e operações atômicas são necessários para garantir exclusão mútua, sincronização, ordenação e visibilidade entre threads.