Trilha 09 · Concorrência e paralelismo

Mutex e semáforos

O mutex (mutual exclusion) é a primitiva de sincronização mais fundamental: garante que apenas uma thread execute a seção crítica por vez. O semáforo generaliza isso para N threads simultâneas. Esses dois mecanismos são a base de toda sincronização em sistemas concorrentes.

① Intuição

O mutex como chave de banheiro

Um mutex é como a chave de um banheiro com porta trancável: somente quem tem a chave pode entrar. Se outra pessoa tentar, fica esperando do lado de fora. Quando a primeira pessoa sai, entrega a chave para quem estava esperando.

Um semáforo é como um estacionamento com N vagas: o sinal verde libera quando há vaga, o vermelho bloqueia quando está cheio. Quando um carro sai, o contador aumenta e o próximo carro pode entrar.

Invariante do mutex: em qualquer instante, no máximo uma thread detém o lock. Isso garante que a seção crítica seja atomicamente executada do ponto de vista das outras threads — mesmo que internamente sejam muitas instruções de CPU.
② Visualização interativa

Mutex em ação: duas threads, uma seção crítica

Avance passo a passo e veja como T1 adquire o lock, T2 bloqueia, e a execução se alterna sem sobreposição na seção crítica.

Thread 1
aguardando
MUTEX
🔓
livre
Thread 2
aguardando
SEÇÃO CRÍTICA — counter += 1
vazia
Duas threads querem entrar na seção crítica (counter += 1). O mutex está livre.
1/8
③ Explicação técnica

Mutex em Python e C

# Python: threading.Lock — o mutex mais simples
import threading

lock = threading.Lock()
counter = 0

def safe_increment():
    global counter
    with lock:            # adquire o lock, libera no final do bloco
        counter += 1       # seção crítica: só 1 thread aqui por vez

# C com pthread (POSIX):
// pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// pthread_mutex_lock(&mutex);    ← bloqueia se já travado
//     counter++;                 ← seção crítica
// pthread_mutex_unlock(&mutex);  ← libera, acorda quem espera

# RLock (reentrant): permite que a MESMA thread trave novamente
rlock = threading.RLock()
with rlock:
    with rlock:   # não bloqueia! mesma thread pode reentrá-lo
        pass     # útil em recursão com lock

Semáforo: até N threads simultâneas

# Semáforo: generalização do mutex
# Mutex = semáforo com valor máximo 1
# Semáforo: permite N threads simultaneamente na seção crítica

import threading

sem = threading.Semaphore(3)   # máx 3 threads simultâneas

def acessar_recurso(i):
    with sem:   # decrementa o contador interno; bloqueia se == 0
        print(f"Thread {i} acessando recurso")
        time.sleep(1)   # simula uso do recurso
    # ao sair: incrementa o contador, acorda uma thread esperando

# Uso clássico: pool de conexões ao banco de dados
# Exemplo: máx 10 conexões simultâneas
db_pool = threading.Semaphore(10)

def query(sql):
    with db_pool:     # garante ≤ 10 queries simultâneas
        conn = get_connection()
        conn.execute(sql)
        release_connection(conn)

# Produtor/Consumidor com semáforos:
# empty = Semaphore(N)  ← slots vazios no buffer
# full  = Semaphore(0)  ← slots cheios no buffer
# Produtor: down(empty), put(item), up(full)
# Consumidor: down(full), get(item), up(empty)
Starvation e prioridade de lock: quando múltiplas threads esperam um mutex, qual acorda primeiro? Depende da implementação. Sem política de fairness, uma thread pode esperar indefinidamente (starvation) se outras threads têm prioridade maior e ficam pegando o lock repetidamente. Locks com fairness (como threading.Lock em CPython) garantem FIFO para evitar starvation. Locks de alta performance (spinlocks) não têm essa garantia.
④ Projeto para programar

Sincronizando acesso a recursos

Mini projeto: implemente um pool de conexões com semáforo: simule 5 conexões de banco disponíveis e 20 threads tentando usá-las. Sem semáforo, print o número de conexões ativas e observe que pode ultrapassar 5. Com Semaphore(5), garanta que nunca excedem 5 simultâneas.

Projeto principal: implemente o problema do produtor/consumidor com buffer circular de tamanho N. Use dois semáforos (empty e full) e um mutex para proteger o buffer. Teste com 3 produtores e 2 consumidores. Garanta que não há race condition, deadlock ou starvation.

Desafio extra: implemente um readers-writers lock (RWLock): múltiplos leitores simultâneos são permitidos, mas um escritor precisa de acesso exclusivo. Implemente usando apenas mutexes e semáforos simples. Garanta que escritores não sofrem starvation quando há muitos leitores contínuos.

⑤ Exercícios rápidos

Teste sua intuição

O que é um semáforo e como ele difere de um mutex?
O que é deadlock por reentrância (reentrant deadlock)?
Quando usar mutex vs semáforo?
⑥ Aplicações no mundo real

Onde você encontra isso

Linux Kernel — spinlock vs mutex

O kernel Linux usa dois tipos de lock: spinlock (a thread fica em busy-wait, consumindo CPU — adequado para seções críticas muito curtas em código de interrupção) e mutex (a thread dorme e é acordada — adequado para esperas longas). Escolher o tipo errado pode causar deadlock (mutex em contexto de interrupção) ou desperdiçar CPU (spinlock por muito tempo).

🐘

PostgreSQL — MVCC e locks

O PostgreSQL usa MVCC (Multi-Version Concurrency Control) para leituras sem bloqueio: cada transação vê um snapshot consistente do banco sem precisar de lock de leitura. Locks só são necessários para escritas conflitantes. O sistema de lock do Postgres tem 8 níveis de granularidade — de ACCESS SHARE (leitura) a ACCESS EXCLUSIVE (DDL) — com detecção automática de deadlock.

🦀

Rust — sincronização sem race conditions

O Rust proíbe race conditions em tempo de compilação via ownership e o sistema de tipos. Mutex<T> envolve um valor T — para acessá-lo você precisa adquirir o lock, que retorna um MutexGuard. Quando o guard sai de escopo, o lock é liberado automaticamente. Se você tentar acessar T sem o lock, o compilador rejeita. Zero race conditions garantidas sem overhead de runtime.

← Anterior: Race conditions Próxima: Filósofos e deadlock →