Trilha de aprendizado · Nível 13 · Tutorial 11

Proteger estado compartilhado entre corrotinas

Ao concluir, você será capaz de identificar regras de estado vulneráveis a intercalações em await e protegê-las com asyncio.Lock quando a exclusão mútua for necessária.

  • Nível: Avançado
  • Duração: 16 min
  • 8 passos
Proteger estado compartilhado entre corrotinas

O que você vai percorrer

  1. Localizar a corrida nos pontos de suspensão Veja como duas tarefas podem aprovar a mesma última vaga ao intercalar execução em um ponto de suspensão. 2 min
  2. Proteger a operação com asyncio.Lock Use uma única instância de asyncio.Lock para manter a regra de vagas enquanto tarefas concorrentes consultam, aguardam e atualizam o mesmo estado. 2 min
  3. Definir o menor trecho protegido que mantém a regra Delimite a seção crítica pela regra de estado: proteja o que precisa permanecer coerente, sem reter o bloqueio durante trabalho independente. 2 min
  4. Liberar o bloqueio sem confundir isso com desfazer alterações Entenda o que acontece com o bloqueio e com o estado quando uma tarefa falha ou é cancelada. 2 min
  5. Evitar dependências que causam deadlock Reconheça ciclos de espera com asyncio.Lock e reorganize as aquisições para que as tarefas possam avançar. 2 min
  6. Respeitar a fronteira entre corrotinas e threads Escolha o mecanismo de sincronização conforme quem acessa o estado: tarefas do mesmo laço ou threads. 2 min
  7. Reduzir o compartilhamento antes de adicionar bloqueios Quando cada tarefa produz um resultado independente, retornar valores e consolidá-los ao final pode eliminar a mutação compartilhada — e também a necessidade de um bloqueio. 2 min
  8. Aplicar e verificar a proteção do estado Pratique a correção de uma corrida em um controle de vagas e verifique propriedades do estado, falhas e cancelamento sem depender de uma ordem fixa de execução. 3 min

O que você vai aprender

  • Localizar uma condição de corrida entre tarefas que executam no mesmo laço.
  • Proteger uma operação composta com asyncio.Lock e async with.
  • Distinguir sincronização entre corrotinas de sincronização entre threads.
  • Reconhecer quando reduzir o compartilhamento é mais simples que acrescentar bloqueios.

Antes de começar

  • Coordenar tarefas com asyncio.TaskGroup
  • Proteger estado compartilhado entre threads

Passo 1 de 8

Localizar a corrida nos pontos de suspensão

Veja como duas tarefas podem aprovar a mesma última vaga ao intercalar execução em um ponto de suspensão.

A regra que o estado precisa manter

Uma única vaga, no máximo uma confirmação

Imagine um controle de vagas mantido apenas em memória. A regra é: o total de reservas confirmadas nunca pode ultrapassar a capacidade.

Com capacidade 1, se uma tarefa consulta que há vaga, essa informação só continua válida enquanto nenhuma outra tarefa puder alterar o estado relevante. Em código assíncrono, uma suspensão entre consultar e confirmar abre espaço para outra tarefa fazer a mesma consulta.

Duas consultas, uma só vaga

As duas tarefas observam o mesmo estado antes de qualquer atualização.

Diagrama com capacidade de uma vaga, zero reservas confirmadas e duas tarefas apontando para a mesma vaga disponível.

Quando ambas leem “0 reservas de 1 vaga”, ambas podem decidir aprovar — embora só uma aprovação seja válida.

A suspensão separa decisão e alteração

Exemplo vulnerável

Execute este script para expor a intercalação; o resultado não deve ser usado como garantia de uma ordem específica.

python
import asyncio

CAPACIDADE = 1
reservas_confirmadas = 0

async def tentar_reservar(nome: str) -> bool:
    global reservas_confirmadas

    if reservas_confirmadas >= CAPACIDADE:
        print(f"{nome}: sem vaga")
        return False

    print(f"{nome}: encontrei vaga")
    await asyncio.sleep(0)  # cede uma oportunidade de execução
    reservas_confirmadas += 1
    print(f"{nome}: confirmei")
    return True

async def main() -> None:
    global reservas_confirmadas

    async with asyncio.TaskGroup() as grupo:
        grupo.create_task(tentar_reservar("Ana"))
        grupo.create_task(tentar_reservar("Bruno"))

    print(f"Reservas finais: {reservas_confirmadas}")
    assert reservas_confirmadas <= CAPACIDADE, "Capacidade ultrapassada!"

asyncio.run(main())

Leia o await como fronteira de validade

await asyncio.sleep(0) cria explicitamente uma oportunidade para o laço executar outra tarefa pronta. Assim, Ana pode consultar o estado, suspender; Bruno pode consultar o mesmo estado, suspender; e só depois ambas registram uma reserva.

Todo await é um ponto em que a tarefa pode suspender, mas não há promessa de que toda expressão aguardada de fato vá suspender naquela execução. Aqui, sleep(0) serve para revelar a falha em um exemplo curto — não para impor uma ordem confiável entre tarefas.

Atenção

Uma thread não elimina a corrida

As tarefas normalmente executam na mesma thread do laço, mas isso não torna uma regra composta automaticamente segura. A corrida surge porque leitura, decisão e atualização foram separadas por uma possível suspensão.

Reconheça a intercalação vulnerável

Ordene os eventos

Organize a sequência que permite duas confirmações quando há apenas uma vaga. Os itens já estão na ordem correta.

  1. Ana alcança `await asyncio.sleep(0)` e cede execução.
  2. Bruno alcança `await asyncio.sleep(0)` e cede execução.
  3. Bruno consulta `reservas_confirmadas` e também encontra 0.
  4. Ana consulta `reservas_confirmadas` e encontra 0.
  5. Bruno retoma e incrementa o contador para 2.
  6. Ana retoma e incrementa o contador para 1.

Passo 2 de 8

Proteger a operação com asyncio.Lock

Use uma única instância de asyncio.Lock para manter a regra de vagas enquanto tarefas concorrentes consultam, aguardam e atualizam o mesmo estado.

Um Lock compartilhado para a mesma regra

Exclusão mútua entre tarefas

No controle de vagas, a regra é: não confirmar mais reservas do que a capacidade disponível. Quando várias tarefas participam dessa regra no mesmo laço, elas devem usar a mesma instância de asyncio.Lock.

async with lock permite que apenas uma tarefa execute a operação protegida por vez. Se o Lock já estiver ocupado, a próxima tarefa aguarda cooperativamente: a thread do laço continua disponível para executar outras tarefas.

Uma chave compartilhada

O Lock não pertence a cada reserva; ele protege o estado compartilhado vagas. A tarefa que entra primeiro mantém a chave até concluir a consulta, a decisão e a atualização.

Diagrama mostrando duas tarefas concorrentes compartilhando um único cadeado antes de acessar um contador de vagas; uma tarefa está na seção protegida e a outra espera fora dela.

Uma única instância de Lock coordena todas as tarefas que alteram a mesma regra de estado.

A operação inteira fica no contexto

Reserva protegida

Passe o mesmo Lock a cada tarefa que disputa as vagas.

python
import asyncio

async def confirmar_reserva(
    cliente: str,
    vagas: dict[str, int],
    lock: asyncio.Lock,
) -> bool:
    async with lock:
        if vagas["disponiveis"] == 0:
            print(f"{cliente}: sem vaga")
            return False

        # Simula uma etapa assíncrona que faz parte da confirmação.
        await asyncio.sleep(0)
        vagas["disponiveis"] -= 1
        print(f"{cliente}: reserva confirmada")
        return True

async def main() -> None:
    vagas = {"disponiveis": 1}
    lock = asyncio.Lock()

    async with asyncio.TaskGroup() as grupo:
        grupo.create_task(confirmar_reserva("Ana", vagas, lock))
        grupo.create_task(confirmar_reserva("Bruno", vagas, lock))

    print(vagas)

asyncio.run(main())

Dica

Delimite pela regra

A consulta vagas["disponiveis"] == 0, a decisão e a subtração formam uma única operação lógica. Como há uma suspensão entre elas neste exemplo, todas precisam ficar sob o mesmo async with lock para que a decisão continue válida.

Erros de escopo que não protegem a decisão

Não basta proteger só a escrita

Este padrão continua vulnerável: a tarefa consulta vagas antes de adquirir o Lock. Enquanto ela aguarda o Lock, outra tarefa pode consumir a última vaga. Ao entrar, a decisão anterior já está desatualizada.

Também não crie asyncio.Lock() dentro de cada chamada de confirmar_reserva: cada tarefa receberia uma chave diferente e nenhuma exclusão mútua ocorreria entre elas.

Padrão insuficiente

A consulta ocorre fora da proteção compartilhada.

python
# Não use este padrão para a regra de vagas.
if vagas["disponiveis"] > 0:
    await asyncio.sleep(0)
    async with lock:
        vagas["disponiveis"] -= 1

Complete a proteção

Mesmo Lock, operação completa

Complete as duas lacunas com a expressão adequada:

lock = asyncio.____()

async def reservar(vagas, lock):
    async ____ lock:
        if vagas["disponiveis"] > 0:
            await asyncio.sleep(0)
            vagas["disponiveis"] -= 1

Passo 3 de 8

Definir o menor trecho protegido que mantém a regra

Delimite a seção crítica pela regra de estado: proteja o que precisa permanecer coerente, sem reter o bloqueio durante trabalho independente.

A regra define o limite

Proteja a relação entre os dados

A seção crítica não é definida apenas pela linha que altera uma variável. Ela deve conter todas as etapas que precisam enxergar um estado coerente para preservar a invariante.

Para reservas, a regra é: confirmadas nunca pode ultrapassar capacidade. Portanto, a consulta da vaga, a decisão e o incremento pertencem à mesma unidade quando uma suspensão pode separá-los.

O recorte da seção crítica

A preparação não depende do contador compartilhado. Já consultar, decidir e registrar a reserva dependem umas das outras.

Diagrama de fluxo separando preparação independente fora do bloqueio de consulta, decisão e atualização do total de reservas dentro de um bloqueio.

Mantenha sob o mesmo lock as etapas cuja relação protege a regra de vagas.

Exemplo

Um limite orientado pela invariante

Se confirmadas é compartilhada, este é o núcleo que precisa ser exclusivo:

async with lock:
    if confirmadas < capacidade:
        confirmadas += 1
        return True
    return False

Não há await nesse núcleo. Entre tarefas do mesmo laço, operações síncronas simples executadas continuamente não se intercalam. Isso vale somente nesse contexto: não é uma garantia geral de atomicidade e não protege acessos de threads.

Reduza a espera sob o lock

O laço continua; quem precisa do lock espera

Manter um async with lock durante um await não bloqueia a thread do laço: outras tarefas ainda podem executar. Porém, toda tarefa que precise desse mesmo lock ficará esperando sua liberação.

Por isso, deixe fora do lock o trabalho independente — por exemplo, montar uma mensagem ou calcular dados locais. Depois, adquira o lock e consulte o estado atual antes de decidir.

Preparar antes, decidir com estado atual

A espera de preparação não participa da regra de vagas, então fica fora da seção crítica.

python
import asyncio

capacidade = 1
confirmadas = 0
lock = asyncio.Lock()

async def tentar_reserva(nome: str) -> bool:
    global confirmadas

    # Trabalho independente do estado compartilhado.
    mensagem = f"Pedido de {nome} pronto"
    await asyncio.sleep(0)

    # Consulta, decisão e alteração formam uma unidade.
    async with lock:
        if confirmadas >= capacidade:
            print(f"{mensagem}: sem vaga")
            return False
        confirmadas += 1
        print(f"{mensagem}: reserva confirmada")
        return True

Dica

Pergunta-guia

Antes de mover um await para fora, pergunte: “A decisão tomada antes dessa espera ainda será válida quando eu voltar?” Se a resposta for não, consulte e decida novamente após adquirir o lock.

Não use uma decisão antiga

Uma reorganização insuficiente

Este código reduz o tempo sob o lock, mas quebra a regra: tem_vaga pode ficar desatualizado enquanto a tarefa aguarda.

python
async def tentar_reserva_incorreta() -> bool:
    global confirmadas

    async with lock:
        tem_vaga = confirmadas < capacidade

    await asyncio.sleep(0)  # outra tarefa pode confirmar a última vaga

    if tem_vaga:
        async with lock:
            confirmadas += 1
        return True
    return False

Menor não significa fragmentado

Separar a leitura da decisão e da alteração só é seguro se você reavaliar a condição protegida. Caso contrário, duas tarefas podem guardar tem_vaga = True e ambas incrementarem depois.

Há casos em que uma espera realmente faz parte da operação exclusiva. Neles, ela deve permanecer dentro do lock para que nenhuma outra tarefa observe ou altere o estado intermediário. O custo é que as demais tarefas dependentes desse lock terão de aguardar.

Escolha o trecho correto

Reserva com preparação assíncrona

Uma tarefa precisa aguardar uma preparação que não lê nem altera confirmadas. Qual organização preserva a regra de capacidade e evita manter o lock sem necessidade?

Passo 4 de 8

Liberar o bloqueio sem confundir isso com desfazer alterações

Entenda o que acontece com o bloqueio e com o estado quando uma tarefa falha ou é cancelada.

Dois efeitos independentes

Liberar não é reverter

Depois que uma tarefa adquiriu um asyncio.Lock, sair de async with lock libera o bloqueio tanto no fluxo normal quanto se o corpo terminar por exceção ou por cancelamento. Isso permite que outra tarefa tente prosseguir.

Essa liberação não desfaz nenhuma alteração no estado. Se a tarefa já diminuiu o número de vagas e depois falhou, a diminuição continua lá. Portanto, organize a seção crítica para que o estado já satisfaça sua regra antes de cada ponto que possa suspender.

Bloqueio e estado seguem destinos diferentes

Observe a diferença entre o recurso de sincronização e os dados alterados.

Diagrama com uma tarefa que adquire um cadeado, altera um contador de vagas e é cancelada; o cadeado é liberado, mas o contador mantém o valor já alterado.

A saída de async with solta o Lock; ela não restaura o contador.

Adie a mutação final até depois da espera

Uma ordem que preserva a regra

Neste exemplo, a espera por uma confirmação é cancelável. Enquanto ela acontece, nenhuma vaga foi consumida. Só depois de a espera terminar a reserva é registrada como uma alteração simples e consistente.

Não capture asyncio.CancelledError para seguir como se nada tivesse ocorrido: deixe-o se propagar após a limpeza automática do contexto.

Reserva com alteração final consistente

Execute este exemplo em um script Python 3.12 ou superior.

python
import asyncio

vagas = 1
lock = asyncio.Lock()


async def confirmar_pagamento() -> None:
    # Representa uma operação cancelável que ainda não mudou `vagas`.
    await asyncio.sleep(0)


async def reservar(nome: str) -> bool:
    global vagas

    async with lock:
        if vagas == 0:
            return False

        await confirmar_pagamento()
        vagas -= 1
        print(f"{nome}: reserva confirmada; vagas = {vagas}")
        return True


async def main() -> None:
    tarefa = asyncio.create_task(reservar("Ana"))
    await asyncio.sleep(0)
    tarefa.cancel()

    try:
        await tarefa
    except asyncio.CancelledError:
        print(f"Ana: cancelada; vagas = {vagas}")

    # O Lock foi liberado pela saída cancelada do async with.
    await reservar("Bruno")


asyncio.run(main())

Cancelamento antes de adquirir

Nem toda tarefa que espera possui o Lock

Uma tarefa pode ser cancelada enquanto está aguardando async with lock adquirir o bloqueio. Nesse caso, ela nunca entra no corpo do contexto: não altera o estado protegido e não deve tentar liberar o Lock manualmente.

A liberação automática só se aplica quando a aquisição foi bem-sucedida e a tarefa efetivamente entrou no contexto.

Dois momentos possíveis para o cancelamento

Compare o cancelamento enquanto a tarefa espera com o cancelamento depois da aquisição.

Comparação em dois painéis: no primeiro, uma tarefa cancelada aguarda um cadeado ocupado e não entra na área protegida; no segundo, uma tarefa dentro da área protegida é cancelada e o cadeado é liberado na saída.

Antes da aquisição, não há corpo protegido nem Lock a liberar; depois da aquisição, a saída do contexto o libera.

Verifique a distinção

Lock liberado, estado preservado

Se uma tarefa decrementa vagas dentro de async with lock e é cancelada logo depois, a saída do contexto libera o Lock e restaura automaticamente o valor anterior de vagas.

Cancelamento durante a espera

Se uma tarefa é cancelada enquanto aguarda adquirir um Lock ocupado, ela não executa o corpo de async with lock e não precisa liberar esse Lock.

Passo 5 de 8

Evitar dependências que causam deadlock

Reconheça ciclos de espera com asyncio.Lock e reorganize as aquisições para que as tarefas possam avançar.

Quando o bloqueio vira uma espera sem saída

Deadlock entre corrotinas

Um asyncio.Lock evita que duas tarefas alterem uma regra ao mesmo tempo, mas não resolve dependências circulares. Há deadlock quando cada tarefa envolvida espera por algo que só outra tarefa bloqueada poderia liberar.

O laço de eventos pode continuar executando tarefas sem relação com o ciclo. Ainda assim, as tarefas presas não fazem progresso.

Três padrões de espera circular

Observe as setas: cada uma representa uma espera que impede a próxima ação necessária.

Diagrama com três situações de deadlock: uma tarefa tentando adquirir novamente seu próprio lock; duas tarefas segurando locks diferentes e esperando uma pela outra; uma tarefa segurando um lock enquanto aguarda outra tarefa que precisa desse lock.

Reaquisição, ordem incompatível de locks e espera por tarefa dependente podem formar ciclos.

Remova a dependência circular

Responsabilidade única pela aquisição

Não execute o trecho incorreto como está: ele foi reduzido para evidenciar a espera.

python
import asyncio

lock = asyncio.Lock()

# INCORRETO: registrar tenta adquirir um lock já mantido
# pela própria tarefa chamadora.
async def registrar() -> None:
    async with lock:
        print("registro concluído")

async def reservar_incorreto() -> None:
    async with lock:
        await registrar()  # espera sem saída

# CORRETO: reservar é responsável por adquirir o lock;
# a função interna pressupõe que ele já está adquirido.
def _registrar_com_lock() -> None:
    print("registro concluído")

async def reservar() -> None:
    async with lock:
        _registrar_com_lock()

# Se duas operações precisarem dos locks A e B, todas devem
# adquiri-los na mesma ordem: primeiro A, depois B.
async def operar_com_dois_locks(lock_a: asyncio.Lock, lock_b: asyncio.Lock) -> None:
    async with lock_a:
        async with lock_b:
            print("operação exclusiva")

# Evite também manter lock e aguardar uma tarefa que precisa dele.
# Reorganize o fluxo para que a tarefa dependente termine antes da
# aquisição, ou para que a espera aconteça depois de liberar o lock.

Dica

Perguntas antes de usar await

Antes de aguardar dentro de async with lock, pergunte: “A conclusão deste aguardável precisa deste mesmo lock, direta ou indiretamente?” Se precisar, reorganize a responsabilidade ou a sequência. Para múltiplos locks, defina e siga uma ordem única de aquisição em todo o programa.

Analise o grafo de espera

Encontre o ciclo

A tarefa A adquiriu contas_lock e agora aguarda historico_lock. Ao mesmo tempo, a tarefa B adquiriu historico_lock e aguarda contas_lock.

Qual é o ciclo de espera? Proponha uma mudança de organização que permita progresso sem depender de tempos de espera.

Escreva pelo menos 80 caracteres (0/80).

Passo 6 de 8

Respeitar a fronteira entre corrotinas e threads

Escolha o mecanismo de sincronização conforme quem acessa o estado: tarefas do mesmo laço ou threads.

Dois domínios, duas regras

O bloqueio precisa alcançar todos os acessos

asyncio.Lock coordena tarefas que executam no mesmo laço de eventos. Ele não torna seguro o acesso de uma thread que manipula diretamente o mesmo objeto.

Já threading.Lock coordena threads. Apesar dos nomes parecidos, os dois bloqueios pertencem a domínios diferentes e não são substitutos intercambiáveis.

Fronteira de sincronização

Observe quem participa de cada regra de exclusão.

Diagrama com tarefas de um mesmo laço protegidas por asyncio.Lock e uma thread separada sem cobertura desse bloqueio.

Um asyncio.Lock cobre as tarefas do seu laço, mas não uma thread que acessa o objeto diretamente.

Não bloqueie a thread do laço

Atenção

threading.Lock pode paralisar o progresso

Se uma corrotina tenta adquirir um threading.Lock que outra tarefa do mesmo laço já mantém, essa aquisição é bloqueante: a própria thread do laço para. Assim, a tarefa que detém o bloqueio não consegue retomar para liberá-lo.

Para disputa entre corrotinas do mesmo laço, use asyncio.Lock com async with: a espera cede o controle ao laço, em vez de bloquear sua thread.

Exemplo

Ciclo de bloqueio

  1. A tarefa A adquire um threading.Lock e chega a um await.
  2. A tarefa B tenta adquirir o mesmo threading.Lock.
  3. B bloqueia a thread do laço.
  4. A não pode retomar após o await para liberar o bloqueio.

O problema não é falta de uma thread extra: é usar uma espera bloqueante dentro da thread que precisa continuar executando o laço.

to_thread amplia a fronteira

Uma função na thread é outro participante

Ao enviar uma função com asyncio.to_thread, a função roda em uma thread trabalhadora. Se ela acessa estado compartilhado, esse acesso passa a ser concorrente com o das corrotinas.

O asyncio.Lock usado pelas corrotinas não protege automaticamente essa função. Mantenha o estado sob responsabilidade de um único domínio ou use uma estratégia apropriada para o lado síncrono, sem fazer o laço esperar em uma aquisição bloqueante.

Dica

Pergunta de revisão

Antes de escolher um bloqueio, liste quem pode tocar o objeto: apenas tarefas de um laço, apenas threads, ou ambos. A resposta define a fronteira que a sincronização precisa cobrir.

Associe o mecanismo ao domínio

Quem esse mecanismo coordena?

Associe cada situação à interpretação correta.

Toque em um item e depois no par correspondente.

Passo 7 de 8

Reduzir o compartilhamento antes de adicionar bloqueios

Quando cada tarefa produz um resultado independente, retornar valores e consolidá-los ao final pode eliminar a mutação compartilhada — e também a necessidade de um bloqueio.

Resultados próprios, consolidação única

Elimine o compartilhamento quando ele não é necessário

Se cada cálculo ou consulta é independente, não faça cada corrotina atualizar um acumulador comum. Faça cada tarefa manter seus valores temporários locais e retornar seu resultado. Depois que o TaskGroup terminar com sucesso, uma única parte do programa consolida esses retornos.

Assim, durante a execução concorrente não há estado mutável compartilhado para proteger. Isso simplifica o raciocínio e pode dispensar asyncio.Lock.

Duas formas de agregar resultados

Comparação visual: à esquerda, várias tarefas escrevem em um único acumulador compartilhado; à direita, cada tarefa produz um resultado próprio que segue para uma consolidação final única.

Resultados independentes fluem para um único ponto de consolidação; não são mutados concorrentemente em um acumulador.

Retorne valores das tarefas

Agregação sem acumulador compartilhado

Cada corrotina retorna um subtotal. A soma só acontece após a saída bem-sucedida do TaskGroup.

python
import asyncio


async def calcular_subtotal(valores: list[int]) -> int:
    await asyncio.sleep(0)  # simula uma operação assíncrona
    return sum(valor * 2 for valor in valores)


async def main() -> None:
    lotes = [[3, 5], [2, 7], [11]]

    async with asyncio.TaskGroup() as grupo:
        tarefas = [
            grupo.create_task(calcular_subtotal(lote))
            for lote in lotes
        ]

    total = sum(tarefa.result() for tarefa in tarefas)
    print(total)  # 56


asyncio.run(main())

Dica

Referências diferentes não bastam

Dar a cada tarefa uma variável com nome diferente não cria isolamento se essas variáveis apontarem para a mesma lista, dicionário ou instância mutável. O isolamento relevante aqui é cada tarefa produzir seu próprio valor — por exemplo, um int retornado — sem alterar um objeto comum.

Escolha a estratégia pela regra

Quando retornar resultados resolve?

Compare dois casos: (1) cada tarefa calcula o subtotal de um lote independente; (2) duas tarefas tentam confirmar a última vaga disponível. Explique por que retornar resultados e consolidar ao final resolve o primeiro caso, mas não resolve sozinho o segundo.

Escreva pelo menos 120 caracteres (0/120).

Passo 8 de 8

Aplicar e verificar a proteção do estado

Pratique a correção de uma corrida em um controle de vagas e verifique propriedades do estado, falhas e cancelamento sem depender de uma ordem fixa de execução.

Roteiro de decisão antes de alterar o código

Quatro perguntas para revisar

Ao encontrar uma regra compartilhada, siga esta sequência:

  1. Qual invariante precisa continuar verdadeira? Aqui: vagas >= 0 e vagas + confirmações == capacidade inicial.
  2. Onde há suspensão? Uma decisão tomada antes de um await pode ficar desatualizada.
  3. O estado realmente precisa ser compartilhado? A disputa pela última vaga precisa; resultados independentes poderiam ser retornados e consolidados depois.
  4. Qual trecho precisa ser exclusivo? Neste caso, consultar vagas, passar pela espera simulada e confirmar precisam usar a mesma instância de asyncio.Lock.

O limite da seção crítica

A proteção envolve a decisão inteira, e não somente a subtração final.

Diagrama de fluxo mostrando duas tarefas chegando à consulta de uma vaga. Um cadeado envolve consulta, espera assíncrona, confirmação e atualização; fora do cadeado ficam preparação e apresentação do resultado.

Se a decisão depende de um estado que pode mudar durante a espera, a consulta e a alteração pertencem à mesma seção crítica.

Prática local: corrija o controle de vagas

Execute no seu computador

Crie um arquivo vagas.py com o código abaixo e execute python vagas.py em Python 3.12 ou superior. A função confirmar está vulnerável de propósito: duas tarefas podem observar a última vaga antes de qualquer atualização.

Em seguida, substitua somente a função confirmar pela versão protegida da próxima tela. Os demais cenários verificam disputa, exceção e cancelamento.

Script inicial com a corrida

python
import asyncio

CAPACIDADE = 1


def verificar(estado: dict[str, object]) -> None:
    vagas = estado["vagas"]
    confirmadas = estado["confirmadas"]
    assert isinstance(vagas, int)
    assert isinstance(confirmadas, list)
    assert vagas >= 0
    assert vagas + len(confirmadas) == CAPACIDADE


async def confirmar(
    nome: str,
    estado: dict[str, object],
    lock: asyncio.Lock,
    *,
    falhar: bool = False,
) -> bool:
    # VULNERÁVEL: esta função ainda não usa lock.
    vagas = estado["vagas"]
    assert isinstance(vagas, int)
    if vagas == 0:
        return False

    await asyncio.sleep(0)
    if falhar:
        raise RuntimeError(f"falha simulada de {nome}")

    estado["vagas"] = vagas - 1
    confirmadas = estado["confirmadas"]
    assert isinstance(confirmadas, list)
    confirmadas.append(nome)
    verificar(estado)
    return True


async def bloqueada_ate_cancelamento(
    lock: asyncio.Lock, entrou: asyncio.Event
) -> None:
    async with lock:
        entrou.set()
        await asyncio.Event().wait()


async def main() -> None:
    lock = asyncio.Lock()

    # Disputa por uma única vaga.
    estado: dict[str, object] = {"vagas": CAPACIDADE, "confirmadas": []}
    resultados = await asyncio.gather(
        confirmar("Ana", estado, lock),
        confirmar("Bia", estado, lock),
        return_exceptions=True,
    )
    print("disputa:", resultados, estado)

    # Falha antes da alteração final: o estado deve continuar consistente.
    estado = {"vagas": CAPACIDADE, "confirmadas": []}
    try:
        await confirmar("Caio", estado, lock, falhar=True)
    except RuntimeError as erro:
        print("exceção observada:", erro)
    verificar(estado)
    print("após exceção:", estado)

    # Cancelamento após adquirir o lock, mas antes de qualquer mutação.
    estado = {"vagas": CAPACIDADE, "confirmadas": []}
    entrou = asyncio.Event()
    tarefa = asyncio.create_task(bloqueada_ate_cancelamento(lock, entrou))
    await entrou.wait()
    tarefa.cancel()
    try:
        await tarefa
    except asyncio.CancelledError:
        print("tarefa cancelada")

    # Esta chamada só termina se o lock foi liberado na saída cancelada.
    resultado_sonda = await confirmar("Sonda", estado, lock)
    verificar(estado)
    print("sonda após cancelamento:", resultado_sonda, estado)


asyncio.run(main())

Correção e critérios de verificação

Substitua confirmar por esta versão

A mesma instância lock, recebida por todas as tarefas que disputam as vagas, cobre a consulta, a suspensão simulada e a mutação consistente.

python
async def confirmar(
    nome: str,
    estado: dict[str, object],
    lock: asyncio.Lock,
    *,
    falhar: bool = False,
) -> bool:
    async with lock:
        vagas = estado["vagas"]
        assert isinstance(vagas, int)
        if vagas == 0:
            return False

        # Está dentro da seção crítica porque a decisão ainda depende
        # da vaga que acabamos de consultar.
        await asyncio.sleep(0)
        if falhar:
            raise RuntimeError(f"falha simulada de {nome}")

        estado["vagas"] = vagas - 1
        confirmadas = estado["confirmadas"]
        assert isinstance(confirmadas, list)
        confirmadas.append(nome)
        verificar(estado)
        return True

Dica

O que deve ser observado

Após a correção, na disputa há exatamente um True e um False, em qualquer ordem; o estado termina com zero vagas e uma confirmação. Após a exceção, o estado continua com uma vaga e nenhuma confirmação. Após o cancelamento, a Sonda consegue confirmar: isso evidencia que o lock foi liberado, não que alterações seriam desfeitas automaticamente.

Atenção

Evidência não é prova geral

O sleep(0) torna a intercalação relevante neste exemplo, mas não estabelece uma ordem garantida. As propriedades verificadas pelo script são mais importantes que a ordem das mensagens. Um teste bem-sucedido aumenta a confiança no cenário exercitado; não prova sozinho a ausência de toda corrida possível.

Revisão aplicada

Justifique sua solução

Relate o que você alterou e o que observou. Indique: (1) a invariante verificada; (2) por que a espera ficou dentro do async with; (3) o que a Sonda demonstra após o cancelamento; e (4) uma situação em que retornos independentes eliminariam a necessidade de lock.

Escreva pelo menos 120 caracteres (0/120).

Resumo

Decisão final

  • Comece pela invariante e localize os pontos em que uma decisão pode atravessar um await.
  • Compartilhe uma única instância de asyncio.Lock entre as tarefas que participam da mesma regra.
  • Proteja consulta, decisão e alteração quando elas precisam permanecer coerentes; não presuma uma ordem de execução.
  • A saída de async with libera um lock adquirido até em falha ou cancelamento, mas não restaura mutações anteriores.
  • asyncio.Lock não sincroniza threads. Quando o trabalho é independente, retornar resultados e consolidá-los pode remover o compartilhamento.

Tutorial concluído

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

Você aplicou uma estratégia de proteção baseada na invariante, verificou progresso após cancelamento e reconheceu os limites de asyncio.Lock.

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