[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.

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 x inicializada com x=0. Duas threads querem executar "incrementar x em 1". O código clássico (não-atomico) é:
  • Thread A: read r_A \leftarrow x; r_A \leftarrow r_A + 1; write x \leftarrow r_A.
  • Thread B: read r_B \leftarrow x; r_B \leftarrow r_B + 1; write x \leftarrow r_B.
Sequência possível (interleaving) que causa erro:
  1. A lê x (obtém 0).
  2. B lê x (obtém 0).
  3. A escreve x \leftarrow 1.
  4. B escreve x \leftarrow 1.
Resultado final: x=1, embora duas threads tenham incrementado — deveria ser x=2. 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 x).
  • 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 x: 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 N usa dois semáforos: empty (inicia com N) e full (inicia com 0); produtor faz wait(empty); put(item); signal(full);, consumidor faz wait(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):
  1. Identifique quais dados/recursos são compartilhados.
  2. Identifique os trechos que modificam esses recursos (seções críticas).
  3. Mostre um exemplo de interleaving que causa erro (como o contador x acima). Isso prova que há problema.
  4. Proponha um mecanismo concreto (mutex, semáforo, cond var, atomic) e mostre onde colocar lock/unlock ou wait/signal. Escreva o pseudocódigo protegido.
  5. Analise efeitos colaterais: risco de deadlock? starvation? custo de desempenho? proponha mitigação.
  6. 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.

Exercícios relacionados