
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.
Trilha de aprendizado · Nível 13 · Tutorial 11
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.
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
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
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
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
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
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
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
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

Passo 1 de 8
Veja como duas tarefas podem aprovar a mesma última vaga ao intercalar execução em um ponto de suspensã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.
As duas tarefas observam o mesmo estado antes de qualquer atualização.

Quando ambas leem “0 reservas de 1 vaga”, ambas podem decidir aprovar — embora só uma aprovação seja válida.
Execute este script para expor a intercalação; o resultado não deve ser usado como garantia de uma ordem específica.
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())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
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.
Organize a sequência que permite duas confirmações quando há apenas uma vaga. Os itens já estão na ordem correta.

Passo 2 de 8
Use uma única instância de asyncio.Lock para manter a regra de vagas enquanto tarefas concorrentes consultam, aguardam e atualizam o mesmo estado.
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.
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.

Uma única instância de Lock coordena todas as tarefas que alteram a mesma regra de estado.
Passe o mesmo Lock a cada tarefa que disputa as vagas.
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
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.
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.
A consulta ocorre fora da proteção compartilhada.
# Não use este padrão para a regra de vagas.
if vagas["disponiveis"] > 0:
await asyncio.sleep(0)
async with lock:
vagas["disponiveis"] -= 1Complete 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
Delimite a seção crítica pela regra de estado: proteja o que precisa permanecer coerente, sem reter o bloqueio durante trabalho independente.
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.
A preparação não depende do contador compartilhado. Já consultar, decidir e registrar a reserva dependem umas das outras.

Mantenha sob o mesmo lock as etapas cuja relação protege a regra de vagas.
Exemplo
Se confirmadas é compartilhada, este é o núcleo que precisa ser exclusivo:
async with lock:
if confirmadas < capacidade:
confirmadas += 1
return True
return FalseNã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.
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.
A espera de preparação não participa da regra de vagas, então fica fora da seção crítica.
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
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.
Este código reduz o tempo sob o lock, mas quebra a regra: tem_vaga pode ficar desatualizado enquanto a tarefa aguarda.
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
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.
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
Entenda o que acontece com o bloqueio e com o estado quando uma tarefa falha ou é cancelada.
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.
Observe a diferença entre o recurso de sincronização e os dados alterados.

A saída de async with solta o Lock; ela não restaura o contador.
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.
Execute este exemplo em um script Python 3.12 ou superior.
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())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.
Compare o cancelamento enquanto a tarefa espera com o cancelamento depois da aquisição.

Antes da aquisição, não há corpo protegido nem Lock a liberar; depois da aquisição, a saída do contexto o libera.
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.
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
Reconheça ciclos de espera com asyncio.Lock e reorganize as aquisições para que as tarefas possam avançar.
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.
Observe as setas: cada uma representa uma espera que impede a próxima ação necessária.

Reaquisição, ordem incompatível de locks e espera por tarefa dependente podem formar ciclos.
Não execute o trecho incorreto como está: ele foi reduzido para evidenciar a espera.
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
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.
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
Escolha o mecanismo de sincronização conforme quem acessa o estado: tarefas do mesmo laço ou threads.
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.
Observe quem participa de cada regra de exclusão.

Um asyncio.Lock cobre as tarefas do seu laço, mas não uma thread que acessa o objeto diretamente.
Atenção
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
threading.Lock e chega a um await.threading.Lock.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.
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
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 cada situação à interpretação correta.
Toque em um item e depois no par correspondente.

Passo 7 de 8
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.
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.

Resultados independentes fluem para um único ponto de consolidação; não são mutados concorrentemente em um acumulador.
Cada corrotina retorna um subtotal. A soma só acontece após a saída bem-sucedida do TaskGroup.
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
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.
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
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.
Ao encontrar uma regra compartilhada, siga esta sequência:
vagas >= 0 e vagas + confirmações == capacidade inicial.await pode ficar desatualizada.asyncio.Lock.A proteção envolve a decisão inteira, e não somente a subtração final.

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.
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.
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())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.
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 TrueDica
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
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.
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
await.asyncio.Lock entre as tarefas que participam da mesma regra.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.Parabéns! Você concluiu: Proteger estado compartilhado entre corrotinas
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