Trilha de aprendizado · Nível 13 · Tutorial 4

Proteger estado compartilhado entre threads

Ao concluir, você será capaz de identificar atualizações concorrentes inseguras e proteger uma regra de estado com threading.Lock, evitando bloqueios desnecessários.

  • Nível: Avançado
  • Duração: 20 min
  • 9 passos
Proteger estado compartilhado entre threads

O que você vai percorrer

  1. Identificar a regra que a concorrência pode quebrar Reconheça quando uma regra de reserva depende da ordem em que as threads leem, decidem e alteram o mesmo estado. 2 min
  2. Reproduzir a corrida com Event Controle uma intercalação específica para tornar visível a corrida pela última vaga, sem depender de atrasos arbitrários. 3 min
  3. Proteger a operação completa com Lock Use um único bloqueio para tornar atômica a regra de reserva: consultar, decidir e atualizar. 3 min
  4. Usar o mesmo bloqueio em todos os caminhos relevantes Associe o estado compartilhado a uma única instância de Lock e audite os métodos que leem ou alteram a regra de vagas. 2 min
  5. Encurtar a seção crítica sem quebrar a regra Organize uma reserva para manter a decisão e a atualização juntas, sem reter o bloqueio durante trabalho que não depende do estado compartilhado. 2 min
  6. Reconhecer e evitar deadlocks Analise dependências de espera entre threads e Locks para reconhecer ciclos que impedem o progresso e reorganizar o código com segurança. 2 min
  7. Reduzir o estado compartilhado Use resultados independentes e uma consolidação central quando a regra permitir adiar a alteração do estado agregado. 2 min
  8. Verificar a versão protegida Verifique a regra de reserva com início coordenado, coleta completa dos resultados e asserções que não dependem de qual thread vence. 3 min
  9. Aplicação final: revisar uma reserva concorrente Revise uma implementação de reservas, compare uma falha controlada com a versão protegida e consolide os critérios para proteger estado compartilhado entre threads. 3 min

O que você vai aprender

  • Identificar condições de corrida em operações compostas sobre estado compartilhado.
  • Proteger uma seção crítica com o mesmo Lock em todos os caminhos relevantes.
  • Reconhecer situações que podem causar deadlock.
  • Verificar uma intercalação problemática com sincronização explícita, sem depender de atrasos arbitrários.

Antes de começar

  • Executar tarefas com ThreadPoolExecutor
  • Controlar espera e encerramento de tarefas em executores
  • Testar mudanças de estado em objetos

Passo 1 de 9

Identificar a regra que a concorrência pode quebrar

Reconheça quando uma regra de reserva depende da ordem em que as threads leem, decidem e alteram o mesmo estado.

A regra da última vaga

Uma regra, não apenas um contador

Considere uma capacidade de 1 vaga. Duas threads tentam executar reservar() ao mesmo tempo.

A regra do estado é: cada reserva aceita consome uma vaga, e o total de reservas aceitas nunca pode exceder a capacidade.

Uma condição de corrida ocorre quando o resultado passa a depender da intercalação das threads. Aqui, não basta perguntar se o contador termina negativo: duas reservas podem ser aceitas usando a mesma única vaga e o contador ainda terminar em 0.

Duas decisões sobre a mesma vaga

As duas threads podem consultar o mesmo estado antes que qualquer uma registre sua alteração.

Diagrama de duas threads lendo uma única vaga disponível, ambas decidindo aceitar a reserva e depois ambas gravando zero vagas restantes.

A falha está em aceitar duas reservas para uma capacidade de uma vaga; o valor final do contador pode ocultar essa violação.

Uma operação lógica em três partes

Reserva aparentemente simples

Cada linha é compreensível isoladamente. Juntas, elas precisam respeitar a regra da capacidade.

python
vagas = 1


def reservar() -> bool:
    global vagas
    if vagas > 0:           # 1. leitura e decisão
        vagas -= 1         # 2. atualização
        return True
    return False

O intervalo vulnerável

A reserva é uma única operação lógica: consultar as vagas atuais, decidir se há disponibilidade e registrar o consumo.

Mas a execução pode ser intercalada entre essas etapas. Por exemplo, a thread A lê 1; antes de atualizar, a thread B também lê 1. As duas decidem aceitar. Depois, ambas podem gravar um estado equivalente a 0.

O GIL, quando habilitado, não transforma essa regra de negócio inteira em uma operação indivisível. Não baseie a correção em detalhes de bytecode, na versão do interpretador ou em uma execução que "pareceu dar certo".

Reconstrua a intercalação problemática

Coloque os eventos na ordem

Ordene uma intercalação que faz as duas threads aceitarem a única vaga. Os eventos de gravação representam o resultado final vagas = 0 em cada caminho.

  1. Thread A lê vagas = 1 e decide aceitar
  2. Thread A grava vagas = 0 e retorna True
  3. Thread B grava vagas = 0 e retorna True
  4. Thread B lê vagas = 1 e decide aceitar

Diagnóstico da regra violada

Qual é o defeito?

Com capacidade inicial de 1, A e B retornam True, e o contador termina em 0. Qual regra foi violada?

Resumo

O que localizar antes de proteger

  • Procure uma regra de estado que combina leitura, decisão e alteração.
  • Pergunte se uma segunda thread pode observar o estado antes de a primeira concluir essa operação lógica.
  • Avalie invariantes do domínio, como “aceites não excedem a capacidade”, em vez de confiar somente no contador final.
  • No próximo passo, você controlará essa intercalação de forma explícita para reproduzir a falha.

Passo 2 de 9

Reproduzir a corrida com Event

Controle uma intercalação específica para tornar visível a corrida pela última vaga, sem depender de atrasos arbitrários.

Uma intercalação controlada

Forçando as duas leituras

Para demonstrar a corrida de forma reproduzível, use Event como pontos de controle. Cada worker sinaliza que já leu vagas; depois, ambos aguardam uma autorização comum para continuar até a atualização.

Assim, o coordenador só libera as escritas quando as duas threads já decidiram com base no mesmo valor. Isso expõe a falha; não protege a regra de reserva.

Sequência imposta pelos eventos

Os eventos separam a observação do estado da alteração posterior.

Diagrama de duas threads que leem uma única vaga disponível, sinalizam eventos de leitura distintos, esperam uma autorização comum e então ambas atualizam o estado.

O coordenador espera os dois sinais de leitura e só então libera a continuação das duas threads.

Atenção

Não use sleep para criar a corrida

sleep apenas atrasa uma thread por um tempo aproximado; não garante que a outra tenha chegado a uma etapa específica. Aqui, o retorno de wait(timeout=...) confirma se o sinal esperado realmente ocorreu. O prazo diagnostica falhas de coordenação — ele não impõe a ordem.

Script completo para executar

Crie e execute localmente

No seu computador, salve o código abaixo como corrida_reserva.py e execute python corrida_reserva.py. Não altere a ordem dos set() no bloco finally: se uma verificação falhar, eles ainda liberam workers que estejam esperando.

corrida_reserva.py

python
from concurrent.futures import ThreadPoolExecutor
from threading import Event

vagas = 1
leitura_ana = Event()
leitura_bia = Event()
pode_atualizar = Event()


def reservar(nome: str, leitura_concluida: Event) -> bool:
    global vagas

    vagas_observadas = vagas
    leitura_concluida.set()

    if not pode_atualizar.wait(timeout=1):
        raise RuntimeError(f"{nome}: autorização não recebida")

    if vagas_observadas > 0:
        vagas -= 1
        print(f"{nome}: reserva aceita; vagas agora = {vagas}")
        return True

    print(f"{nome}: reserva recusada")
    return False


with ThreadPoolExecutor(max_workers=2) as executor:
    futura_ana = executor.submit(reservar, "Ana", leitura_ana)
    futura_bia = executor.submit(reservar, "Bia", leitura_bia)

    try:
        ana_leu = leitura_ana.wait(timeout=1)
        bia_leu = leitura_bia.wait(timeout=1)
        if not (ana_leu and bia_leu):
            raise RuntimeError("Nem todas as leituras foram confirmadas")

        pode_atualizar.set()

        resultados = [futura_ana.result(), futura_bia.result()]
        print(f"Aceites: {sum(resultados)}; vagas finais: {vagas}")
    finally:
        pode_atualizar.set()
        leitura_ana.set()
        leitura_bia.set()

Dica

O papel de cada sinal

leitura_ana e leitura_bia registram etapas distintas, uma para cada worker. pode_atualizar é a autorização compartilhada para prosseguir. Um Event permanece sinalizado depois de set(), por isso ele pode liberar ambos os workers.

Leia o resultado como uma falha de regra

O que o script deve revelar

As duas reservas devem ser aceitas, apesar de existir apenas uma vaga. O valor final tende a ficar em -1: cada thread tomou sua decisão usando vagas_observadas == 1 antes que qualquer uma diminuísse o estado compartilhado.

A ordem dos prints pode variar. O defeito demonstrado não depende de qual thread aparece primeiro, mas do fato de ambas terem recebido autorização depois de observar a mesma vaga.

Explique a intercalação

Após executar o script, relate: qual resultado viola a capacidade, quais eventos forçaram a intercalação e por que substituir essa coordenação por sleep não seria confiável.

Escreva pelo menos 120 caracteres (0/120).

Passo 3 de 9

Proteger a operação completa com Lock

Use um único bloqueio para tornar atômica a regra de reserva: consultar, decidir e atualizar.

Uma unidade lógica, uma região protegida

O que o Lock garante

Na reserva da última vaga, a consulta, a decisão e a atualização formam uma única operação lógica. Um threading.Lock compartilhado cria exclusão mútua: enquanto uma thread atravessa a região protegida, outra que tente adquirir o mesmo bloqueio espera.

Associe o bloqueio ao estado que ele protege e adquira-o com with. Ao sair do bloco, o Python libera o Lock automaticamente.

Fronteira da seção crítica

A fronteira correta envolve as três etapas dependentes do estado atual.

Diagrama de uma única caixa protegida por cadeado contendo as etapas consultar vagas, validar disponibilidade e decrementar vagas; duas threads chegam à entrada da caixa, mas somente uma está dentro.

A decisão é segura porque usa a leitura mais recente feita já dentro da região protegida.

Reserva protegida

Leitura, decisão e escrita sob o mesmo Lock

O mesmo lock é usado sempre que este método executa a regra de reserva.

python
from threading import Lock


class Vagas:
    def __init__(self, capacidade: int) -> None:
        self._disponiveis = capacidade
        self._lock = Lock()

    def reservar(self) -> bool:
        with self._lock:
            if self._disponiveis == 0:
                return False

            self._disponiveis -= 1
            return True

Por que funciona

A thread que entra no with consulta self._disponiveis, decide e, se houver vaga, decrementa o valor antes de liberar o Lock. A próxima thread só poderá fazer sua própria consulta depois disso; portanto, verá o estado atualizado.

O return dentro do with não mantém o bloqueio preso: a saída do bloco libera o Lock antes de o método retornar.

Atenção

Escrita isolada não basta

Não basta colocar apenas self._disponiveis -= 1 dentro de with self._lock. Se a thread leu que havia vaga antes de adquirir o Lock, sua decisão ainda pode se basear em um estado desatualizado. A consulta decisiva e a validação precisam estar na mesma região protegida que a atualização.

Complete a fronteira correta

Preencha o início da seção crítica

Complete a linha para que a leitura, a validação e a alteração ocorram sob o Lock:

def reservar(self) -> bool:
    ___ self._lock:
        if self._disponiveis == 0:
            return False
        self._disponiveis -= 1
        return True

Liberação não é desfazer

Saída segura, estado preservado

Se a reserva for aceita, o decremento já ocorreu quando o método sai do with. A liberação do Lock apenas permite que outra thread entre na região protegida; ela não restaura automaticamente o estado anterior.

Da mesma forma, se uma exceção sair do bloco, o Lock é liberado. Isso evita deixar outras threads esperando para sempre, mas não implementa rollback das alterações feitas antes da exceção. Se a regra exigir reversão, ela deve ser programada explicitamente.

Verifique a distinção

Verdadeiro ou falso: se self._disponiveis -= 1 já foi executado e uma exceção ocorre depois, a saída do with self._lock restaura automaticamente o valor anterior.

Passo 4 de 9

Usar o mesmo bloqueio em todos os caminhos relevantes

Associe o estado compartilhado a uma única instância de Lock e audite os métodos que leem ou alteram a regra de vagas.

Um protocolo compartilhado

O Lock precisa ser o mesmo

Um Lock coordena apenas as threads que tentam adquirir aquela mesma instância. Criar Lock() dentro de cada chamada cria bloqueios independentes: cada thread pode entrar em sua própria região “protegida” ao mesmo tempo.

Para uma regra de vagas, associe um único bloqueio ao objeto que guarda as vagas. Reserva, devolução e uma consulta que precise retratar um estado coerente devem seguir esse mesmo protocolo.

Caminhos que convergem no mesmo Lock

Diagrama mostrando três operações — reservar, devolver e consultar — passando pelo mesmo cadeado antes de acessar um contador de vagas. Ao lado, cadeados separados em cada caminho não coordenam o acesso ao mesmo contador.

A proteção existe entre caminhos somente quando todos usam a mesma instância de Lock.

Estado e bloqueio no mesmo objeto

Dois erros de identidade do bloqueio

Nenhuma das versões abaixo coordena chamadas concorrentes entre si.

python
import threading

class Vagas:
    def __init__(self, capacidade: int) -> None:
        self.disponiveis = capacidade

    def reservar(self) -> bool:
        lock = threading.Lock()  # novo Lock a cada chamada
        with lock:
            if self.disponiveis == 0:
                return False
            self.disponiveis -= 1
            return True

    def devolver(self) -> None:
        with threading.Lock():  # outro Lock, diferente do anterior
            self.disponiveis += 1

Um único protocolo para as vagas

O atributo _lock protege o atributo _disponiveis. Os acessos relevantes ficam concentrados nos métodos da classe.

python
import threading

class Vagas:
    def __init__(self, capacidade: int) -> None:
        self._disponiveis = capacidade
        self._lock = threading.Lock()

    def reservar(self) -> bool:
        with self._lock:
            if self._disponiveis == 0:
                return False
            self._disponiveis -= 1
            return True

    def devolver(self) -> None:
        with self._lock:
            self._disponiveis += 1

    def disponiveis(self) -> int:
        with self._lock:
            return self._disponiveis

Dica

Não contorne a fronteira

Evite expor _disponiveis ou uma coleção mutável interna para que outro código a altere diretamente. Se uma leitura participa de uma decisão ou precisa ser coerente com a regra, ofereça um método que faça essa leitura sob o mesmo Lock.

Audite os caminhos de acesso

Qual implementação mantém o protocolo?

Uma classe controla self._disponiveis. Qual alternativa permite que reserva, devolução e consulta consistente participem da mesma proteção?

Passo 5 de 9

Encurtar a seção crítica sem quebrar a regra

Organize uma reserva para manter a decisão e a atualização juntas, sem reter o bloqueio durante trabalho que não depende do estado compartilhado.

O que fica dentro — e o que fica fora

Seção crítica curta, regra intacta

Depois de definir a fronteira correta da seção crítica, reduza-a apenas removendo trabalho que não participa da regra.

Em uma reserva, a consulta das vagas, a decisão de aceitar ou recusar e o desconto da vaga devem continuar juntos sob o mesmo Lock. Já preparar uma mensagem, formatar dados ou enviar uma confirmação pode ficar fora.

Manter trabalho desnecessário sob o bloqueio aumenta a contenção: outras threads ficam esperando mesmo quando não precisam disputar o estado compartilhado.

Três momentos da operação

A posição da etapa importa: a validação e a atualização ficam no centro, como uma única unidade protegida.

Diagrama de fluxo com preparação antes de um portão de bloqueio, consulta e atualização de uma vaga dentro do portão, e processamento de confirmação depois do portão.

Prepare antes; consulte, valide e altere dentro; processe os dados capturados depois.

Capture o resultado e libere antes do trabalho lento

Reorganize sem separar a decisão da escrita

A preparação de nome e a confirmação posterior não determinam se ainda há vaga. Por isso, podem ocorrer fora do bloqueio. O comprovante é capturado enquanto a reserva é confirmada e usado depois que o with libera o Lock.

Reserva com bloqueio enxuto

A chamada de confirmação ocorre somente depois da saída do bloco protegido.

python
from threading import Lock


class Agenda:
    def __init__(self, vagas: int) -> None:
        self._vagas = vagas
        self._lock = Lock()

    def reservar(self, nome: str) -> bool:
        # Trabalho independente: não precisa do estado protegido.
        solicitacao = nome.strip().title()

        with self._lock:
            # Consulta, decisão e alteração formam uma única regra.
            if self._vagas == 0:
                return False

            self._vagas -= 1
            comprovante = f"Reserva confirmada para {solicitacao}"

        # Trabalho potencialmente lento: Lock já foi liberado.
        enviar_confirmacao(comprovante)
        return True


def enviar_confirmacao(comprovante: str) -> None:
    print(comprovante)

Atenção

Não espere sob o Lock sem necessidade

Evite chamadas externas, operações demoradas e futuro.result() enquanto mantém o bloqueio. Em especial, se o trabalhador aguardado precisar adquirir esse mesmo Lock, ele não avança e a espera pode impedir o progresso. Libere o bloqueio antes de esperar, quando a espera não fizer parte da regra de estado.

Organize uma reserva corretamente

Antes, durante e depois

Coloque as etapas na ordem adequada para reservar uma vaga e enviar uma confirmação. Considere que a mensagem de confirmação só é necessária quando a reserva foi aceita.

  1. Sair do bloco protegido e enviar a confirmação.
  2. Entrar no bloco `with lock`.
  3. Consultar a quantidade atual de vagas.
  4. Capturar o comprovante da reserva aceita.
  5. Validar que há vaga e decrementar a quantidade.
  6. Preparar o nome da pessoa e os dados da solicitação.

Passo 6 de 9

Reconhecer e evitar deadlocks

Analise dependências de espera entre threads e Locks para reconhecer ciclos que impedem o progresso e reorganizar o código com segurança.

Quando a espera deixa de progredir

Deadlock não é só contenção

Há contenção quando uma thread espera um Lock que outra thread vai liberar em breve. Há deadlock quando as dependências de espera formam uma situação em que ninguém consegue dar o próximo passo para liberar o que os demais precisam.

O caso clássico: a thread 1 mantém o Lock A e espera o Lock B; ao mesmo tempo, a thread 2 mantém B e espera A. Nenhuma delas chega ao fim do seu bloco with.

Ciclo de aquisição circular

Observe que cada thread possui um recurso e aguarda o recurso possuído pela outra.

Diagrama com duas threads e dois cadeados: thread 1 segura A e aguarda B; thread 2 segura B e aguarda A, formando um ciclo fechado.

A espera circular impede que qualquer uma das threads libere o Lock que já possui.

Atenção

Não execute o exemplo de falha

Um exemplo que realmente cria um deadlock pode deixar o programa preso. Aqui, use os trechos apenas para inspecionar as dependências; o diagnóstico vem do ciclo, não de tentar reproduzi-lo com atrasos arbitrários.

Eliminar a aquisição circular

Mesma ordem para todos os caminhos

O trecho inseguro adquire Locks em ordens opostas. A correção define uma ordem global: primeiro lock_a, depois lock_b, em qualquer operação que precise dos dois.

python
from threading import Lock

lock_a = Lock()
lock_b = Lock()

# NÃO EXECUTE: ordens opostas podem formar um ciclo.
def transferir_ab_inseguro():
    with lock_a:
        with lock_b:
            atualizar()

def transferir_ba_inseguro():
    with lock_b:
        with lock_a:
            atualizar()

# Seguro quanto à ordem: toda operação adquire A antes de B.
def atualizar_dois_recursos():
    with lock_a:
        with lock_b:
            atualizar()

# transferir_ab() e transferir_ba() devem chamar atualizar_dois_recursos().

Dica

Uma camada assume a aquisição

Centralize a aquisição dos dois Locks em uma função ou método responsável pela operação inteira. Funções internas que trabalham sob essa proteção não devem tentar adquirir novamente esses Locks. Assim, a ordem fica visível e consistente.

Outras dependências que travam

Reaquisição e espera por Future

threading.Lock não é reentrante. Se um método entra em with self._lock: e chama outro método que tenta entrar no mesmo with self._lock:, a própria thread fica esperando por um Lock que ela não liberará.

Também há bloqueio de progresso quando uma thread mantém um Lock e chama future.result() para esperar um trabalhador que precisa daquele Lock. A thread coordenadora espera o trabalhador; o trabalhador espera a coordenadora liberar o recurso.

Dependências a remover

Estes são padrões para diagnosticar, não para executar. Em ambos, a correção remove a dependência: a camada externa é a única a adquirir o Lock, e a espera pelo Future ocorre após liberá-lo.

python
from threading import Lock

class Estoque:
    def __init__(self):
        self._lock = Lock()
        self._vagas = 1

    # NÃO EXECUTE: reservar() já mantém self._lock.
    def reservar(self):
        with self._lock:
            return self._tem_vaga()  # tenta adquirir o mesmo Lock

    def _tem_vaga(self):
        with self._lock:
            return self._vagas > 0

    # Correção: método interno pressupõe Lock já adquirido.
    def _tem_vaga_protegida(self):
        return self._vagas > 0

# Padrão inseguro:
# with lock:
#     futuro = executor.submit(trabalhar_com_recurso)
#     resultado = futuro.result()  # trabalhador precisa de lock
# Correção: saia do with antes de futuro.result(),
# ou reorganize para o trabalhador não depender desse Lock.

Diagnosticar a dependência

Associe o cenário à correção

Relacione cada padrão de espera à reorganização que remove sua dependência problemática.

Toque em um item e depois no par correspondente.

Passo 7 de 9

Reduzir o estado compartilhado

Use resultados independentes e uma consolidação central quando a regra permitir adiar a alteração do estado agregado.

Em vez de acumular nas threads

Resultados locais, consolidação central

Se cada tarefa pode calcular seu resultado sem alterar dados em comum, faça-a retornar esse valor. A thread coordenadora coleta os retornos e é a única a atualizar o acumulador.

Assim, as threads não disputam o mesmo contador, lista ou dicionário durante o trabalho. Não há uma seção crítica a proteger para essa agregação, porque a mutação acontece em um único ponto.

Fluxo sem mutação concorrente

Cada trabalhador lê sua entrada e devolve um resultado próprio; somente o coordenador altera o total.

Diagrama mostrando três trabalhadores com entradas separadas que retornam resultados ao coordenador, que atualiza sozinho um acumulador central.

Retorne valores independentes; consolide o estado agregado em uma única thread.

Exemplo: somar sem acumulador compartilhado

Cada Future devolve sua parte

Execute no seu computador para observar uma agregação feita apenas depois da conclusão das tarefas.

python
from concurrent.futures import ThreadPoolExecutor


def contar_pares(numeros: tuple[int, ...]) -> int:
    # Lê apenas a entrada recebida e devolve um resultado independente.
    return sum(numero % 2 == 0 for numero in numeros)


lotes = ((2, 3, 4), (5, 6, 7), (8, 9, 10))

with ThreadPoolExecutor(max_workers=3) as executor:
    futuros = [executor.submit(contar_pares, lote) for lote in lotes]
    resultados = [futuro.result() for futuro in futuros]

total_de_pares = sum(resultados)
print(total_de_pares)  # 5

Dica

Evite compartilhamento por referência

A entrada deve ser tratada como somente leitura, e um resultado devolvido não deve continuar sendo alterado pelo trabalhador. Retornar uma lista mutável e mantê-la para alterações posteriores recria um caminho de mutação compartilhada.

Quando consolidar não basta

A decisão pode esperar?

Consolidar depois funciona quando cada resultado é válido de forma independente — por exemplo, contar itens, calcular valores ou produzir relatórios.

Já uma reserva da última vaga exige decidir durante o trabalho concorrente: antes de aceitar, a thread precisa consultar a capacidade atual e reduzi-la como uma única regra. Nesse caso, adiar a consolidação permitiria que mais de uma thread aceitasse a mesma vaga; use o mesmo Lock que protege essa regra.

Escolha a estratégia

Três threads analisam arquivos somente para leitura e cada uma calcula quantas linhas têm erro. Ao final, o programa precisa exibir o total. Qual estratégia é mais apropriada?

Passo 8 de 9

Verificar a versão protegida

Verifique a regra de reserva com início coordenado, coleta completa dos resultados e asserções que não dependem de qual thread vence.

O que a verificação deve afirmar

Invariantes, não vencedores

Na versão protegida, o resultado relevante é a regra de negócio: com 1 vaga e 2 tentativas, deve haver exatamente 1 aceite, 1 recusa e 0 vagas restantes.

Não imponha qual thread será aceita. Depois que as duas são liberadas, a ordem de aquisição do Lock continua indeterminada.

Início conjunto, acesso exclusivo

O Event libera as duas chamadas antes da tentativa de adquirir o bloqueio. O Lock deixa apenas uma entrar por vez na operação de reserva.

Diagrama com duas threads liberadas por um mesmo evento, convergindo para um cadeado que protege uma única vaga; uma reserva é aceita e a outra recusada.

A liberação conjunta cria disputa pela vaga, mas não execução simultânea dentro da seção crítica.

Atenção

Não reutilize o teste da corrida

Na reprodução da corrida, duas threads eram mantidas após a leitura para forçar decisões baseadas no mesmo valor antigo. Colocar essa espera dentro da seção protegida agora seria incorreto: a primeira thread manteria o Lock, e a segunda não conseguiria chegar ao mesmo ponto. Isso pode transformar o teste em espera sem progresso, não em uma verificação da proteção.

Script completo para verificar a reserva

Coordene antes do Lock

Copie o script para um arquivo, por exemplo verificar_reserva.py. Os eventos pronto_a e pronto_b confirmam que os trabalhadores chegaram ao portão de início. Só então o coordenador libera ambos com iniciar.

O finally libera o portão mesmo se a checagem de prontidão falhar, para não deixar trabalhadores presos antes de o executor encerrar.

verificar_reserva.py

python
from concurrent.futures import ThreadPoolExecutor
from threading import Event, Lock


class Reservas:
    def __init__(self, capacidade: int) -> None:
        self._restantes = capacidade
        self._lock = Lock()

    def reservar(self) -> bool:
        # Leitura, decisão e atualização formam uma única seção crítica.
        with self._lock:
            if self._restantes == 0:
                return False
            self._restantes -= 1
            return True

    def vagas_restantes(self) -> int:
        # Esta leitura participa da verificação de uma visão coerente.
        with self._lock:
            return self._restantes


def tentar_reserva(
    reservas: Reservas,
    pronto: Event,
    iniciar: Event,
) -> bool:
    pronto.set()
    if not iniciar.wait(timeout=1):
        raise RuntimeError("O início coordenado não foi liberado")
    return reservas.reservar()


def main() -> None:
    capacidade = 1
    reservas = Reservas(capacidade)
    iniciar = Event()
    prontos = [Event(), Event()]

    with ThreadPoolExecutor(max_workers=2) as executor:
        futuros = [
            executor.submit(tentar_reserva, reservas, pronto, iniciar)
            for pronto in prontos
        ]

        try:
            assert all(pronto.wait(timeout=1) for pronto in prontos), (
                "Nem todos os trabalhadores chegaram ao portão de início"
            )
        finally:
            # Libera quem já estiver esperando, inclusive se a asserção falhar.
            iniciar.set()

        resultados = [futuro.result(timeout=1) for futuro in futuros]

    aceites = sum(resultados)
    recusas = len(resultados) - aceites
    restantes = reservas.vagas_restantes()

    assert aceites == 1
    assert recusas == 1
    assert restantes == capacidade - aceites
    assert aceites <= capacidade

    print(f"Resultados: {resultados}")
    print(f"Aceites: {aceites}; recusas: {recusas}; restantes: {restantes}")


if __name__ == "__main__":
    main()

Execute e interprete

Verificação local

No terminal, execute:

python verificar_reserva.py

A lista em Resultados pode ser [True, False] ou [False, True]. As duas saídas são válidas. O que deve permanecer igual é: um aceite, uma recusa e nenhuma vaga restante.

Dica

O alcance do teste

Esta intercalação controlada testa uma situação relevante e as asserções validam suas invariantes. Ainda assim, testes aprovados — mesmo repetidos muitas vezes — não provam que toda corrida possível foi eliminada. Complete a verificação inspecionando se todos os acessos relevantes usam a mesma instância de Lock e se não há esperas dependentes dela.

Explique a verificação

Resultado e desenho do teste

Após executar o script, relate quais resultados são válidos e quais invariantes foram verificadas. Em seguida, explique por que o antigo ponto de espera que exigia duas leituras simultâneas não pode ser colocado dentro da seção protegida pelo Lock.

Escreva pelo menos 180 caracteres (0/180).

Passo 9 de 9

Aplicação final: revisar uma reserva concorrente

Revise uma implementação de reservas, compare uma falha controlada com a versão protegida e consolide os critérios para proteger estado compartilhado entre threads.

Checklist da regra antes de alterar o código

Uma reserva é uma operação indivisível

A invariante é simples: com capacidade inicial de 1, no máximo uma reserva pode ser aceita e cada aceite reduz a quantidade disponível em 1.

Ao revisar o código, procure quatro pontos: a leitura que decide, a validação, a atualização e todos os métodos que acessam esse estado. Eles precisam seguir o mesmo protocolo com a mesma instância de Lock quando participam da regra.

O limite correto da seção crítica

A decisão baseada no saldo atual e sua atualização formam uma única região protegida. Sinalizações e esperas de coordenação ficam fora dela.

Comparação visual entre duas threads lendo uma vaga fora de uma proteção e duas threads passando, uma de cada vez, por uma região protegida que contém leitura, validação e atualização.

À esquerda, duas decisões usam a mesma leitura antiga. À direita, o Lock envolve a regra inteira.

Dica

Pergunta de revisão

Se uma thread usar um Lock criado localmente e outra usar outro Lock, elas não se coordenam. O bloqueio deve pertencer ao estado compartilhado, não a uma chamada isolada.

Execute a comparação controlada

Prática local

Crie um arquivo, por exemplo reservas.py, copie o script completo abaixo e execute python reservas.py. A primeira verificação força as duas threads a tomarem a decisão a partir da mesma leitura. A segunda libera as chamadas juntas, mas deixa o Lock decidir quem entra primeiro na regra.

reservas.py

Versão insegura e versão corrigida, com asserções locais.

python
from concurrent.futures import ThreadPoolExecutor
from threading import Event, Lock


class MesaInsegura:
    def __init__(self, vagas: int) -> None:
        self.vagas = vagas

    def reservar(
        self,
        leitura_feita: Event,
        pode_atualizar: Event,
    ) -> bool:
        vagas_observadas = self.vagas
        leitura_feita.set()

        if not pode_atualizar.wait(timeout=1):
            raise RuntimeError("coordenação da demonstração falhou")

        if vagas_observadas <= 0:
            return False

        self.vagas -= 1
        return True


class MesaSegura:
    def __init__(self, vagas: int) -> None:
        self._vagas = vagas
        self._lock = Lock()

    def reservar(self) -> bool:
        with self._lock:
            if self._vagas <= 0:
                return False
            self._vagas -= 1
            return True

    def devolver_vaga(self) -> None:
        with self._lock:
            self._vagas += 1

    def vagas_disponiveis(self) -> int:
        with self._lock:
            return self._vagas


def demonstrar_corrida() -> None:
    mesa = MesaInsegura(vagas=1)
    leitura_a = Event()
    leitura_b = Event()
    pode_atualizar = Event()

    with ThreadPoolExecutor(max_workers=2) as executor:
        futuro_a = executor.submit(mesa.reservar, leitura_a, pode_atualizar)
        futuro_b = executor.submit(mesa.reservar, leitura_b, pode_atualizar)
        try:
            assert leitura_a.wait(timeout=1), "a primeira leitura não ocorreu"
            assert leitura_b.wait(timeout=1), "a segunda leitura não ocorreu"
        finally:
            # Libera trabalhadores mesmo se uma asserção acima falhar.
            pode_atualizar.set()

        resultados = [futuro_a.result(), futuro_b.result()]

    assert sum(resultados) == 2
    assert mesa.vagas == -1
    print("Corrida controlada:", resultados, "vagas:", mesa.vagas)


def verificar_protecao() -> None:
    mesa = MesaSegura(vagas=1)
    iniciar = Event()

    def tentar_reserva() -> bool:
        if not iniciar.wait(timeout=1):
            raise RuntimeError("início das reservas não foi liberado")
        return mesa.reservar()

    with ThreadPoolExecutor(max_workers=2) as executor:
        futuros = [executor.submit(tentar_reserva) for _ in range(2)]
        iniciar.set()
        resultados = [futuro.result() for futuro in futuros]

    assert sum(resultados) == 1
    assert mesa.vagas_disponiveis() == 0
    print("Versão protegida:", resultados, "vagas:", mesa.vagas_disponiveis())


demonstrar_corrida()
verificar_protecao()

Atenção

O que não deve entrar no Lock

Não mova Event.wait(), Future.result() nem uma chamada externa para dentro de with self._lock:. Uma espera sob o bloqueio pode impedir o trabalhador aguardado de obter esse mesmo recurso. Neste script, os eventos apenas controlam o teste; eles não protegem a regra de reserva.

Justifique a correção

Revisão da implementação

Após executar o script, descreva os dois resultados. Depois, explique por que MesaSegura usa o mesmo Lock em reservar, devolver_vaga e vagas_disponiveis, por que as esperas ficaram fora da seção crítica e por que os testes aprovados não são uma prova absoluta de ausência de corridas.

Escreva pelo menos 180 caracteres (0/180).

Síntese: estado compartilhado sob controle

Resumo

Critérios para a próxima revisão

Você fechou o ciclo de uma reserva concorrente: identificou a corrida, controlou a intercalação sem sleep, protegeu a regra e verificou os resultados sem depender de qual thread venceu.

  • Defina a invariante e proteja, como unidade, a leitura decisiva, a validação e a atualização.
  • Associe uma única instância de Lock ao estado e use-a em todos os caminhos que participam da regra.
  • Mantenha a seção crítica curta: prepare dados e aguarde resultados fora dela quando isso não separar decisão e atualização.
  • Evite dependências circulares, nova aquisição do mesmo Lock não reentrante e esperas por trabalhadores que precisam do recurso mantido.
  • Quando os trabalhadores puderem devolver resultados independentes, consolide-os em um único ponto e reduza a mutação compartilhada.
  • Uma intercalação controlada demonstra um defeito; testes que passam aumentam a confiança, mas não substituem a revisão dos acessos e das dependências.

Tutorial concluído

Parabéns! Você concluiu: Proteger estado compartilhado entre threads

Você revisou e verificou uma reserva concorrente. Ao encontrar estado compartilhado em threads, proteja a regra inteira com o mesmo Lock, evite esperas sob essa proteção e trate os testes como evidência limitada, não como prova definitiva.

Baixe o Aplicativo agora para ter acesso a + de 5000 cursos gratuitos, exercícios, certificado e muito conteúdo sem pagar nada!

  • Cursos online 100% gratuitos do início ao fim

    Milhares de cursos online em vídeo, ebooks e áudiobooks.

  • Mais de 60 mil exercícios gratuitos

    Para testar seus conhecimentos no decorrer dos cursos online

  • Certificado Digital gratuito válido em todo o Brasil

    Gerado diretamente na galeria de fotos do seu celular e enviado ao seu e-mail

Aplicativo Cursa na tela de ebook, na tela de curso em vídeo e na tela de exercícios do curso, mais o certificado de conclusão de curso