
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.
Trilha de aprendizado · Nível 13 · Tutorial 4
Ao concluir, você será capaz de identificar atualizações concorrentes inseguras e proteger uma regra de estado com threading.Lock, evitando bloqueios desnecessários.
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
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
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
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
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
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
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
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
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

Passo 1 de 9
Reconheça quando uma regra de reserva depende da ordem em que as threads leem, decidem e alteram o mesmo estado.
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.
As duas threads podem consultar o mesmo estado antes que qualquer uma registre sua alteração.

A falha está em aceitar duas reservas para uma capacidade de uma vaga; o valor final do contador pode ocultar essa violação.
Cada linha é compreensível isoladamente. Juntas, elas precisam respeitar a regra da capacidade.
vagas = 1
def reservar() -> bool:
global vagas
if vagas > 0: # 1. leitura e decisão
vagas -= 1 # 2. atualização
return True
return FalseA 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".
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.
Com capacidade inicial de 1, A e B retornam True, e o contador termina em 0. Qual regra foi violada?
Resumo

Passo 2 de 9
Controle uma intercalação específica para tornar visível a corrida pela última vaga, sem depender de atrasos arbitrários.
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.
Os eventos separam a observação do estado da alteração posterior.

O coordenador espera os dois sinais de leitura e só então libera a continuação das duas threads.
Atenção
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.
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.
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
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.
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.
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
Use um único bloqueio para tornar atômica a regra de reserva: consultar, decidir e atualizar.
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.
A fronteira correta envolve as três etapas dependentes do estado atual.

A decisão é segura porque usa a leitura mais recente feita já dentro da região protegida.
O mesmo lock é usado sempre que este método executa a regra de reserva.
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 TrueA 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
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 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 TrueSe 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.
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
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 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.

A proteção existe entre caminhos somente quando todos usam a mesma instância de Lock.
Nenhuma das versões abaixo coordena chamadas concorrentes entre si.
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 += 1O atributo _lock protege o atributo _disponiveis. Os acessos relevantes ficam concentrados nos métodos da classe.
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._disponiveisDica
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.
Uma classe controla self._disponiveis. Qual alternativa permite que reserva, devolução e consulta consistente participem da mesma proteção?

Passo 5 de 9
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.
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.
A posição da etapa importa: a validação e a atualização ficam no centro, como uma única unidade protegida.

Prepare antes; consulte, valide e altere dentro; processe os dados capturados depois.
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.
A chamada de confirmação ocorre somente depois da saída do bloco protegido.
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
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.
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.

Passo 6 de 9
Analise dependências de espera entre threads e Locks para reconhecer ciclos que impedem o progresso e reorganizar o código com segurança.
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.
Observe que cada thread possui um recurso e aguarda o recurso possuído pela outra.

A espera circular impede que qualquer uma das threads libere o Lock que já possui.
Atenção
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.
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.
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
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.
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.
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.
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.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
Use resultados independentes e uma consolidação central quando a regra permitir adiar a alteração do estado agregado.
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.
Cada trabalhador lê sua entrada e devolve um resultado próprio; somente o coordenador altera o total.

Retorne valores independentes; consolide o estado agregado em uma única thread.
Execute no seu computador para observar uma agregação feita apenas depois da conclusão das tarefas.
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
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.
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.
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
Verifique a regra de reserva com início coordenado, coleta completa dos resultados e asserções que não dependem de qual thread vence.
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.
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.

A liberação conjunta cria disputa pela vaga, mas não execução simultânea dentro da seção crítica.
Atenção
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.
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.
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()
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
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.
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
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.
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.
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.

À esquerda, duas decisões usam a mesma leitura antiga. À direita, o Lock envolve a regra inteira.
Dica
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.
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.
Versão insegura e versão corrigida, com asserções locais.
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
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.
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).
Resumo
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.
Parabéns! Você concluiu: Proteger estado compartilhado entre threads
Milhares de cursos online em vídeo, ebooks e áudiobooks.
Para testar seus conhecimentos no decorrer dos cursos online
Gerado diretamente na galeria de fotos do seu celular e enviado ao seu e-mail
Baixe nosso aplicativo pelo QR Code ou pelos links abaixo:.
+ de 10 milhões
de alunos
Certificado grátis e
válido em todo o Brasil
60 mil exercícios
gratuitos
4,8/5 classificação
nas lojas de apps
Cursos gratuitos em
vídeo, ebooks e audiobooks