
Passo 1 de 9
Entender o limite de capacidade
Diferencie capacidade compartilhada, exclusão mútua e controle de quantidade de operações por segundo.
Trilha de aprendizado · Nível 13 · Tutorial 12
Ao concluir, você será capaz de controlar quantas operações assíncronas acessam um recurso ao mesmo tempo e limitar a criação de tarefas sem confundir essas duas responsabilidades.
Entender o limite de capacidade
Diferencie capacidade compartilhada, exclusão mútua e controle de quantidade de operações por segundo. 2 min
Compartilhar um semáforo entre as tarefas
Crie uma capacidade única no coordenador e compartilhe essa mesma referência com todas as tarefas que usam o recurso limitado. 2 min
Delimitar a operação que consome o recurso
Escolha o trecho protegido pelo semáforo de acordo com o período em que o recurso está realmente ocupado. 2 min
Preservar permissões diante de falhas
Use o contexto assíncrono para devolver permissões sem esconder falhas ou cancelamentos. 2 min
Escolher o alcance do timeout
Posicione o timeout de acordo com a política: incluir a fila por capacidade ou limitar somente a operação já admitida. 3 min
Separar operações ativas de tarefas existentes
Entenda por que limitar o uso simultâneo de um recurso não limita, por si só, quantas tarefas foram criadas. 2 min
Criar tarefas em lotes finitos
Use lotes para limitar quantas tarefas de processamento existem por vez, mantendo um semáforo compartilhado para limitar as operações que realmente usam o recurso. 3 min
Verificar o limite com execução controlada
Use sinais e invariantes para comprovar o limite de simultaneidade sem depender de rede, arquivos ou durações medidas. 4 min
Aplicar os dois limites e justificar a política
Integre lotes, semáforo e timeout em um fluxo local e verificável, mantendo claras as responsabilidades de cada mecanismo. 3 min

Passo 1 de 9
Diferencie capacidade compartilhada, exclusão mútua e controle de quantidade de operações por segundo.
Um asyncio.Semaphore representa um conjunto de permissões. Sua capacidade define quantas operações podem usar o mesmo recurso ao mesmo tempo.
Se a capacidade é 3, até três tarefas podem estar na operação limitada simultaneamente. Uma quarta tarefa não começa essa operação ainda: ela aguarda cooperativamente até que uma permissão fique disponível.
O semáforo é adequado quando o recurso tem capacidade compartilhada, como um serviço externo que aceita algumas requisições simultâneas.
Observe que as tarefas aguardando ainda existem, mas não estão usando o recurso limitado.

Com capacidade 3, no máximo três operações ficam admitidas simultaneamente.
Você já usou asyncio.Lock para proteger uma regra de estado compartilhado: ele permite uma tarefa por vez na seção crítica.
O semáforo generaliza a ideia para uma capacidade maior que um. Ele não diz que as operações são mutuamente exclusivas; diz que há um número limitado de vagas para elas.
Exemplo
Lock.Semaphore com capacidade 4.Um limite de simultaneidade responde: “quantas operações estão em andamento agora?”
Ele não responde: “quantas operações começaram durante um segundo?”. Se operações curtas terminam rapidamente, permissões podem ser reutilizadas muitas vezes no mesmo intervalo. Portanto, um semáforo não garante um máximo de operações por segundo.
Relacione cada requisito ao controle que o atende diretamente.
Toque em um item e depois no par correspondente.

Passo 2 de 9
Crie uma capacidade única no coordenador e compartilhe essa mesma referência com todas as tarefas que usam o recurso limitado.
Se várias tarefas acessam o mesmo recurso com capacidade limitada, elas precisam disputar as permissões do mesmo asyncio.Semaphore.
Crie-o no coordenador assíncrono com uma capacidade inteira positiva. Depois, passe a referência para cada tarefa. Por exemplo, asyncio.Semaphore(2) permite que até duas operações estejam dentro do trecho limitado ao mesmo tempo.
Todas as tarefas apontam para o mesmo conjunto de permissões.

Um semáforo único coordena todas as tarefas sujeitas ao mesmo limite.
Dica
O escopo de criação acompanha o recurso: se as tarefas devem respeitar uma capacidade conjunta, entregue a elas a mesma instância do semáforo.
Na tarefa, use async with semaforo: ao redor da operação que será limitada. Se não houver permissão disponível, a tarefa aguarda cooperativamente; outras tarefas do laço podem continuar progredindo.
Ao sair normalmente do bloco, a permissão é devolvida e outra tarefa que aguarda pode ser admitida.
A operação simulada só começa depois de obter uma permissão.
import asyncio
async def processar(item: str, semaforo: asyncio.Semaphore) -> str:
async with semaforo:
print(f"início: {item}")
await asyncio.sleep(1) # operação assíncrona simulada
print(f"fim: {item}")
return item.upper()Salve o código como limite.py e execute python limite.py. Os itens podem terminar em ordem variável, mas nunca haverá mais de duas mensagens de início sem liberações correspondentes.
A instância é criada uma vez em main e passada para todas as tarefas do TaskGroup. O exemplo usa somente a biblioteca padrão.
import asyncio
async def processar(item: str, semaforo: asyncio.Semaphore) -> str:
async with semaforo:
print(f"início: {item}")
await asyncio.sleep(1)
print(f"fim: {item}")
return item.upper()
async def main() -> None:
itens = ["a", "b", "c", "d"]
semaforo = asyncio.Semaphore(2)
async with asyncio.TaskGroup() as grupo:
tarefas = [
grupo.create_task(processar(item, semaforo))
for item in itens
]
resultados = [tarefa.result() for tarefa in tarefas]
print(resultados)
asyncio.run(main())Atenção
Isto não impõe um limite conjunto:
async def processar(item: str) -> str:
semaforo = asyncio.Semaphore(2) # instância independente
async with semaforo:
...Cada tarefa ganha seu próprio conjunto de duas permissões. Com várias tarefas, todas podem entrar ao mesmo tempo.
Complete os dois espaços com o mesmo identificador para compartilhar a capacidade:
async def processar(item, ____):
async with ____:
await asyncio.sleep(1)
async def main():
semaforo = asyncio.Semaphore(2)
async with asyncio.TaskGroup() as grupo:
grupo.create_task(processar("a", semaforo))
Passo 3 de 9
Escolha o trecho protegido pelo semáforo de acordo com o período em que o recurso está realmente ocupado.
O semáforo deve ser adquirido imediatamente antes de uma tarefa começar a usar o recurso limitado e liberado somente depois que esse uso terminou de verdade. Preparar dados antes e processar o resultado depois não precisa consumir uma permissão, se essas etapas forem independentes do recurso.
A faixa destacada representa o trecho que deve ficar dentro de async with semaforo.

Proteja o uso efetivo e a finalização que libera o recurso; deixe fora as etapas independentes.
Dica
Pergunte: “Se esta linha ainda estiver executando, o recurso continua ocupado?” Se a resposta for sim, ela pertence ao bloco protegido — inclusive um await de encerramento necessário.
Neste exemplo, enviar() e fechar() representam o uso de um recurso com capacidade limitada, como uma conexão ou uma API externa.
import asyncio
async def preparar(item: str) -> str:
return item.upper()
async def enviar(dado: str) -> str:
await asyncio.sleep(0.2) # recurso está ocupado
return f"resposta para {dado}"
async def fechar() -> None:
await asyncio.sleep(0) # encerramento necessário do uso
async def pos_processar(resposta: str) -> str:
return resposta.replace("resposta", "resultado")
async def processar(item: str, semaforo: asyncio.Semaphore) -> str:
dado = await preparar(item) # independente: fora do limite
async with semaforo:
resposta = await enviar(dado) # recurso ocupado
await fechar() # ainda ocupa até encerrar
return await pos_processar(resposta) # independente: fora do limite
Este padrão não protege o consumo do recurso:
async with semaforo:
tarefa = asyncio.create_task(enviar(dado))
return await tarefaO bloco termina logo após criar a tarefa. Quando enviar() começa a executar, a permissão já foi devolvida; assim, várias tarefas podem usar o recurso ao mesmo tempo. O contexto deve envolver o await da operação que consome o recurso.
Atenção
Proteger create_task(...) limita apenas a criação daquela tarefa durante um instante. Não limita a atividade que ela executará depois, fora do contexto.
preparar() só transforma dados locais. enviar() usa uma conexão limitada. fechar() conclui o uso dessa conexão. formatar() trabalha apenas com o resultado já obtido. Qual é o menor escopo correto?
Em suas palavras, por que envolver apenas create_task(enviar(dado)) com async with semaforo não limita as operações de envio?
Escreva pelo menos 80 caracteres (0/80).

Passo 4 de 9
Use o contexto assíncrono para devolver permissões sem esconder falhas ou cancelamentos.
Depois que uma tarefa entra em async with semaforo:, ela passa a possuir uma permissão. Ao sair desse bloco, a permissão é devolvida automaticamente — tanto ao concluir normalmente quanto se a operação lançar uma exceção ou receber cancelamento.
Esse comportamento mantém a capacidade disponível para as outras tarefas. A exceção ou asyncio.CancelledError continua se propagando: o semáforo cuida da permissão, não do tratamento do erro.
A devolução acontece somente para quem adquiriu a permissão.

Sucesso, falha e cancelamento após a aquisição devolvem uma permissão. Cancelamento enquanto aguarda não cria uma permissão para devolver.
Dica
Prefira async with semaforo: em vez de chamar acquire() e release() manualmente. O contexto associa a devolução exatamente a uma aquisição bem-sucedida.
A operação simulada pode falhar ou ser cancelada em um await.
import asyncio
async def enviar(item: str, semaforo: asyncio.Semaphore) -> str:
async with semaforo:
# A permissão já foi adquirida aqui.
await asyncio.sleep(0.1) # representa uma operação cooperativa
if item == "invalido":
raise ValueError("item recusado")
return f"enviado: {item}"
# Se sleep, a validação ou outra etapa lançar exceção,
# ou se a tarefa for cancelada, o async with devolve a permissão.
# Não capture CancelledError para ignorá-lo; deixe-o se propagar.Atenção
Não chame semaforo.release() dentro ou depois de um bloco async with semaforo:: isso devolve duas permissões para uma única aquisição e pode permitir operações acima do limite.
Também não devolva manualmente uma permissão se a tarefa foi cancelada enquanto ainda aguardava entrar no async with: ela nunca a adquiriu.
Devolver a permissão não encerra, por si só, trabalho externo que já esteja em andamento. Nos exemplos cooperativos, o cancelamento é percebido em pontos de espera; integrações externas precisam ter sua própria política de interrupção e limpeza.
Uma tarefa é cancelada enquanto está aguardando entrar em async with semaforo:. Ela deve chamar semaforo.release() em um finally para compensar o cancelamento.

Passo 5 de 9
Posicione o timeout de acordo com a política: incluir a fila por capacidade ou limitar somente a operação já admitida.
Um semáforo pode fazer uma tarefa esperar antes de usar o recurso. Decida se essa espera faz parte do prazo:
Não há uma posição universalmente correta: o aninhamento deve expressar o requisito.
A borda colorida representa o trecho coberto pelo timeout. A permissão é ocupada somente após a entrada no semáforo.

Timeout externo inclui a espera; timeout interno começa após a admissão.
Coloque asyncio.timeout fora de async with semaforo quando a tarefa não pode ficar aguardando capacidade indefinidamente. O orçamento começa antes da aquisição e continua durante a operação admitida.
Capture TimeoutError depois de sair do contexto de timeout.
import asyncio
async def usar_recurso(semaforo: asyncio.Semaphore, item: str) -> None:
try:
async with asyncio.timeout(5):
async with semaforo:
await operacao_externa(item)
except TimeoutError:
print(f"{item}: prazo total esgotado")
async def operacao_externa(item: str) -> None:
await asyncio.sleep(1)
print(f"{item}: concluído")
Dica
Se o prazo se esgotar depois da aquisição, o cancelamento sai do bloco async with semaforo. A permissão é devolvida na saída desse contexto; não chame release() manualmente.
Coloque asyncio.timeout dentro de async with semaforo quando o requisito é limitar apenas o uso efetivo do recurso. Nesse desenho, a espera pela permissão não tem prazo definido por esse timeout.
A tarefa primeiro espera a permissão. Só então começa o orçamento de 5 segundos.
import asyncio
async def usar_recurso(semaforo: asyncio.Semaphore, item: str) -> None:
async with semaforo:
try:
async with asyncio.timeout(5):
await operacao_externa(item)
except TimeoutError:
print(f"{item}: operação excedeu o prazo")
async def operacao_externa(item: str) -> None:
await asyncio.sleep(1)
print(f"{item}: concluído")
Atenção
O segundo código não impõe limite de espera pela capacidade. Se isso violar o requisito, use o timeout externo ou outra política explícita para a fila.
Relacione o requisito ao posicionamento adequado de asyncio.timeout.
Toque em um item e depois no par correspondente.

Passo 6 de 9
Entenda por que limitar o uso simultâneo de um recurso não limita, por si só, quantas tarefas foram criadas.
Em um fluxo assíncrono, não confunda estas quantidades:
async with semaforo, mas ainda não obtiveram permissão.O asyncio.Semaphore limita apenas as operações ativas dentro do escopo protegido. As demais tarefas podem continuar existindo e aguardando.
Com capacidade 3, apenas três operações usam o recurso de uma vez. Isso não impede que muitas outras tarefas já tenham sido criadas e estejam esperando.

A capacidade controla quem entra no trecho protegido; ela não determina quantas tarefas existem.
Exemplo
import asyncio
async def usar_recurso(item: int, semaforo: asyncio.Semaphore) -> None:
async with semaforo:
await asyncio.sleep(1) # operação limitada
async def main() -> None:
itens = range(1_000)
semaforo = asyncio.Semaphore(3)
async with asyncio.TaskGroup() as grupo:
for item in itens:
grupo.create_task(usar_recurso(item, semaforo))
asyncio.run(main())Aqui, o semáforo permite no máximo 3 operações ativas no async with. Porém, o laço cria até 1.000 tarefas de processamento antes de o TaskGroup terminar. Muitas podem ficar aguardando uma permissão.
O TaskGroup organiza o ciclo de vida: ele espera as tarefas e coordena falhas. Ele não estabelece, sozinho, um máximo para a quantidade de tarefas criadas.
Dica
Reduzir a capacidade de 3 para 1 reduz operações simultâneas, mas não muda o for: ele ainda tenta criar uma tarefa para cada entrada. Se também for necessário conter tarefas pendentes, é preciso adotar uma política adicional para admitir novas tarefas.
Um programa cria 500 tarefas em um TaskGroup. Todas usam a mesma instância de asyncio.Semaphore(4) ao redor da chamada que acessa um serviço externo. Qual afirmação é correta?

Passo 7 de 9
Use lotes para limitar quantas tarefas de processamento existem por vez, mantendo um semáforo compartilhado para limitar as operações que realmente usam o recurso.
Um semáforo não impede que você crie muitas tarefas: elas podem ficar aguardando uma permissão. Para limitar também as tarefas de processamento desse fluxo, forme um lote finito, crie somente as tarefas dele e espere o TaskGroup encerrar antes de iniciar o próximo.
O tamanho do lote limita as tarefas de processamento criadas em cada rodada. A capacidade do semáforo limita as operações que estão usando o recurso ao mesmo tempo. Portanto, as operações ativas ficam limitadas por min(capacidade, tamanho_do_lote).
Cada lote termina antes que o próximo comece; o mesmo semáforo continua valendo para todos eles.

Com lote de 3 e semáforo de capacidade 2, há no máximo 3 tarefas de processamento no lote e no máximo 2 operações usando o recurso.
itertools.batched está disponível no Python 3.12 ou superior. Crie o semáforo uma única vez, antes do laço de lotes. Assim, todas as operações usam a mesma capacidade, em vez de cada lote ou tarefa ganhar um limite independente.
O código processa entradas locais simuladas. Os resultados de um lote são lidos após o TaskGroup e usados imediatamente; as referências às tarefas daquele lote não são guardadas para os próximos.
import asyncio
from itertools import batched
async def processar_item(item: str, semaforo: asyncio.Semaphore) -> str:
# Preparação independente do recurso limitado.
item_normalizado = item.strip()
# Somente o uso simulado do recurso consome uma permissão.
async with semaforo:
await asyncio.sleep(0.1)
resposta = item_normalizado.upper()
# Pós-processamento independente também fica fora do semáforo.
return f"processado: {resposta}"
async def processar_em_lotes(entradas: list[str]) -> None:
tamanho_lote = 3
capacidade = 2
semaforo = asyncio.Semaphore(capacidade)
for numero_lote, lote in enumerate(batched(entradas, tamanho_lote), start=1):
tarefas: list[asyncio.Task[str]] = []
async with asyncio.TaskGroup() as grupo:
for item in lote:
tarefas.append(grupo.create_task(processar_item(item, semaforo)))
# A saída bem-sucedida do grupo garante que estas tarefas terminaram.
resultados_lote = [tarefa.result() for tarefa in tarefas]
print(f"lote {numero_lote}: {resultados_lote}")
async def main() -> None:
entradas = ["ana", "bia", "caio", "davi", "elis"]
await processar_em_lotes(entradas)
asyncio.run(main())A saída do async with asyncio.TaskGroup() espera as tarefas do lote terminarem. Só então o laço pode formar o próximo lote. Se uma tarefa tiver uma falha não tratada, o TaskGroup encerra conforme sua política de falhas e essa falha sai do bloco: os lotes seguintes não são criados.
O último lote pode ter menos itens que o tamanho configurado. Nesse caso, parte da capacidade do semáforo pode ficar ociosa. Isso é esperado: não há tarefas suficientes naquele lote para ocupá-la.
Dica
Lotes evitam criar uma tarefa de processamento para cada entrada de uma vez. Ainda assim, a aplicação pode reter as entradas originais, resultados acumulados ou outros objetos. Neste exemplo, cada resultado é consolidado e exibido por lote, sem guardar referências às tarefas anteriores.
Coloque as etapas na ordem em que ocorrem para um lote.

Passo 8 de 9
Use sinais e invariantes para comprovar o limite de simultaneidade sem depender de rede, arquivos ou durações medidas.
Para verificar um semáforo, substitua a operação externa por uma corrotina que fica parada em um asyncio.Event. Assim, você sabe exatamente quando operações admitidas continuam ocupando as permissões e quando serão liberadas.
Dentro do async with semaforo, conte ativas, atualize o maior valor observado em pico e faça o decremento em finally. Como essas atualizações não têm await entre leitura e escrita, elas não são intercaladas por outra tarefa no mesmo laço de eventos.
O teste deve provar três invariantes: pico <= capacidade, ocupação efetiva da capacidade quando há tarefas suficientes, e ativas == 0 depois do encerramento.
O sinal liberar mantém as operações admitidas no recurso até que o teste decida prosseguir.

A confirmação de capacidade cheia evita que um teste sequencial passe por engano.
Copie o script para um arquivo, por exemplo verificar_semaforo.py, e execute python verificar_semaforo.py. Ele usa somente a biblioteca padrão e verifica sucesso, exceção, cancelamento durante a espera e cancelamento durante uma operação admitida. Após cada caso de falha ou cancelamento, uma nova ocupação completa confirma que nenhuma permissão foi perdida.
import asyncio
from dataclasses import dataclass, field
CAPACIDADE = 2
@dataclass
class Sonda:
capacidade: int
semaforo: asyncio.Semaphore = field(init=False)
liberar: asyncio.Event = field(default_factory=asyncio.Event)
capacidade_cheia: asyncio.Event = field(default_factory=asyncio.Event)
ativas: int = 0
pico: int = 0
def __post_init__(self) -> None:
self.semaforo = asyncio.Semaphore(self.capacidade)
def preparar_onda(self) -> None:
self.liberar = asyncio.Event()
self.capacidade_cheia = asyncio.Event()
self.ativas = 0
self.pico = 0
async def operacao(
self,
*,
falhar: bool = False,
iniciou_tentativa: asyncio.Event | None = None,
) -> None:
# O sinal é emitido antes de tentar adquirir uma permissão.
if iniciou_tentativa is not None:
iniciou_tentativa.set()
async with self.semaforo:
self.ativas += 1
self.pico = max(self.pico, self.ativas)
if self.ativas == self.capacidade:
self.capacidade_cheia.set()
try:
await self.liberar.wait()
if falhar:
raise RuntimeError("falha simulada")
finally:
self.ativas -= 1
async def esperar_capacidade_cheia(sonda: Sonda) -> None:
# Este timeout é apenas uma proteção contra teste travado.
try:
async with asyncio.timeout(1):
await sonda.capacidade_cheia.wait()
except TimeoutError as erro:
raise AssertionError("A capacidade não foi ocupada no teste") from erro
def verificar_encerramento(sonda: Sonda) -> None:
assert sonda.pico <= sonda.capacidade
assert sonda.pico == sonda.capacidade
assert sonda.ativas == 0
async def onda_com_sucesso(sonda: Sonda) -> None:
sonda.preparar_onda()
tarefas = [
asyncio.create_task(sonda.operacao())
for _ in range(sonda.capacidade)
]
await esperar_capacidade_cheia(sonda)
sonda.liberar.set()
await asyncio.gather(*tarefas)
verificar_encerramento(sonda)
async def onda_com_falha(sonda: Sonda) -> None:
sonda.preparar_onda()
tarefas = [
asyncio.create_task(sonda.operacao(falhar=(indice == 0)))
for indice in range(sonda.capacidade)
]
await esperar_capacidade_cheia(sonda)
sonda.liberar.set()
resultados = await asyncio.gather(*tarefas, return_exceptions=True)
assert any(isinstance(resultado, RuntimeError) for resultado in resultados)
verificar_encerramento(sonda)
async def cancelamento_durante_espera(sonda: Sonda) -> None:
sonda.preparar_onda()
ocupantes = [
asyncio.create_task(sonda.operacao())
for _ in range(sonda.capacidade)
]
await esperar_capacidade_cheia(sonda)
tentativa_iniciada = asyncio.Event()
aguardando = asyncio.create_task(
sonda.operacao(iniciou_tentativa=tentativa_iniciada)
)
await tentativa_iniciada.wait()
aguardando.cancel()
try:
await aguardando
except asyncio.CancelledError:
pass
else:
raise AssertionError("A tarefa que aguardava deveria ser cancelada")
sonda.liberar.set()
await asyncio.gather(*ocupantes)
verificar_encerramento(sonda)
async def cancelamento_durante_operacao(sonda: Sonda) -> None:
sonda.preparar_onda()
tarefas = [
asyncio.create_task(sonda.operacao())
for _ in range(sonda.capacidade)
]
await esperar_capacidade_cheia(sonda)
tarefas[0].cancel()
try:
await tarefas[0]
except asyncio.CancelledError:
pass
else:
raise AssertionError("A operação admitida deveria ser cancelada")
sonda.liberar.set()
await asyncio.gather(*tarefas[1:])
verificar_encerramento(sonda)
async def main() -> None:
sonda = Sonda(CAPACIDADE)
await onda_com_sucesso(sonda)
await onda_com_falha(sonda)
await onda_com_sucesso(sonda) # confirma recuperação após falha
await cancelamento_durante_espera(sonda)
await onda_com_sucesso(sonda) # confirma que espera cancelada não devolveu a mais
await cancelamento_durante_operacao(sonda)
await onda_com_sucesso(sonda) # confirma devolução após cancelamento admitido
print("Todas as invariantes foram verificadas.")
if __name__ == "__main__":
asyncio.run(main())capacidade_cheia.wait() só é liberado quando ativas == capacidade. Portanto, cada onda de sucesso demonstra que havia operações suficientes para preencher todas as permissões; não aprovaria uma execução indevidamente sequencial.
Depois que as tarefas terminam, pico <= capacidade demonstra que o limite nunca foi ultrapassado, e ativas == 0 demonstra que o trecho protegido foi encerrado. As ondas posteriores são a evidência adicional de que exceções e cancelamentos não deixaram permissões retidas.
Atenção
Não use sleep, uma ordem supostamente previsível de admissão, atributos privados do semáforo ou uma duração observada como prova do limite. O timeout de 1 segundo no script serve somente para impedir que um defeito deixe o teste esperando para sempre; ele não mede desempenho nem estabelece uma ordem entre tarefas.
Execute o script no seu computador. Relate a mensagem final e explique quais asserções evidenciam: capacidade ocupada, respeito ao limite, encerramento das operações e recuperação após falha ou cancelamento.
Escreva pelo menos 120 caracteres (0/120).

Passo 9 de 9
Integre lotes, semáforo e timeout em um fluxo local e verificável, mantendo claras as responsabilidades de cada mecanismo.
Use configurações independentes: TAMANHO_LOTE limita quantas tarefas de processamento este fluxo cria por vez; CAPACIDADE limita quantas dessas tarefas podem usar o recurso simultaneamente. Neste cenário, o timeout fica fora do async with semaforo: o orçamento inclui tanto a espera por capacidade quanto a operação admitida.
A permissão começa imediatamente antes da operação que usa o recurso e termina quando essa operação retorna ou falha. Preparação de dados e consolidação de resultados ficam fora desse trecho.
A ordem visual mostra onde cada política atua.

O lote limita tarefas existentes; o semáforo limita operações ativas; o timeout define o orçamento do escopo escolhido.
Dica
Se o requisito fosse “depois de admitida, a operação tem até 2 segundos”, o asyncio.timeout ficaria dentro do semáforo. Assim, a espera por uma permissão não teria prazo definido por esse contexto.
Salve o código como limites.py e execute python limites.py em Python 3.12 ou superior. Ele usa apenas a biblioteca padrão. Os Events tornam a verificação controlada: não dependem de uma duração ou ordem específica.
import asyncio
from itertools import batched
CAPACIDADE = 2 # Máximo de operações usando o recurso.
TAMANHO_LOTE = 3 # Máximo de tarefas de processamento por lote.
async def operacao_simulada(item: str) -> str:
"""Substitua pelo uso real do recurso limitado."""
await asyncio.sleep(0)
if item == "falha":
raise RuntimeError("falha simulada")
return item.upper()
async def processar(item: str, semaforo: asyncio.Semaphore, operacao) -> str:
try:
# Política: os 2 segundos incluem esperar capacidade e usar o recurso.
async with asyncio.timeout(2):
async with semaforo:
return await operacao(item)
except TimeoutError:
return f"timeout:{item}"
except RuntimeError as erro:
return f"erro:{item}:{erro}"
async def processar_em_lotes(itens: list[str]) -> list[str]:
semaforo = asyncio.Semaphore(CAPACIDADE) # Uma instância compartilhada.
resultados: list[str] = []
for lote in batched(itens, TAMANHO_LOTE):
# As referências das tarefas vivem somente durante este lote.
async with asyncio.TaskGroup() as grupo:
tarefas = [
grupo.create_task(processar(item, semaforo, operacao_simulada))
for item in lote
]
resultados.extend(tarefa.result() for tarefa in tarefas)
return resultados
class RecursoControlado:
def __init__(self, capacidade: int) -> None:
self.capacidade = capacidade
self.ativas = 0
self.pico = 0
self.capacidade_ocupada = asyncio.Event()
self.liberar = asyncio.Event()
async def usar(self, item: str) -> str:
self.ativas += 1
self.pico = max(self.pico, self.ativas)
if self.ativas == self.capacidade:
self.capacidade_ocupada.set()
try:
if item == "falha":
raise RuntimeError("falha controlada")
await self.liberar.wait()
return item.upper()
finally:
self.ativas -= 1
async def verificar_limites() -> None:
# Sucesso: há tarefas suficientes para ocupar toda a capacidade.
semaforo = asyncio.Semaphore(CAPACIDADE)
recurso = RecursoControlado(CAPACIDADE)
tarefas = [
asyncio.create_task(processar(str(indice), semaforo, recurso.usar))
for indice in range(CAPACIDADE)
]
await asyncio.wait_for(recurso.capacidade_ocupada.wait(), timeout=1)
assert recurso.pico == CAPACIDADE
assert recurso.ativas == CAPACIDADE
recurso.liberar.set()
assert await asyncio.gather(*tarefas) == ["0", "1"]
assert recurso.ativas == 0
# Falha após admissão: async with devolve a permissão automaticamente.
recurso_falha = RecursoControlado(1)
resultado = await processar("falha", semaforo, recurso_falha.usar)
assert resultado.startswith("erro:falha:")
async with semaforo: # A permissão continua disponível.
pass
# Cancelamento enquanto espera: não há acquire bem-sucedido para devolver.
recurso_espera = RecursoControlado(1)
ocupante = asyncio.create_task(processar("ocupante", semaforo, recurso_espera.usar))
await asyncio.wait_for(recurso_espera.capacidade_ocupada.wait(), timeout=1)
chegou_para_esperar = asyncio.Event()
async def aguardar_permissao() -> None:
chegou_para_esperar.set()
async with semaforo:
raise AssertionError("não deveria adquirir durante este cenário")
aguardando = asyncio.create_task(aguardar_permissao())
await chegou_para_esperar.wait()
aguardando.cancel()
try:
await aguardando
except asyncio.CancelledError:
pass
recurso_espera.liberar.set()
await ocupante
# Cancelamento durante a operação também libera a permissão.
recurso_cancelado = RecursoControlado(1)
em_operacao = asyncio.create_task(processar("cancelar", semaforo, recurso_cancelado.usar))
await asyncio.wait_for(recurso_cancelado.capacidade_ocupada.wait(), timeout=1)
em_operacao.cancel()
try:
await em_operacao
except asyncio.CancelledError:
pass
async with semaforo:
pass
async def main() -> None:
resultados = await processar_em_lotes(["ana", "bia", "falha", "davi"])
print(resultados)
await verificar_limites()
print("Verificações concluídas.")
if __name__ == "__main__":
asyncio.run(main())Atenção
Não chame semaforo.release() manualmente dentro de um bloco async with semaforo. Depois de uma aquisição bem-sucedida, o contexto já devolve a permissão em saídas normais, falhas e cancelamentos. Uma devolução extra corrompe o limite.
Após executar o script, relate: (1) o que TAMANHO_LOTE limita; (2) o que CAPACIDADE limita; (3) por que o timeout está fora do semáforo; e (4) quais asserções indicam que não houve perda de permissões.
Escreva pelo menos 180 caracteres (0/180).
Resumo
asyncio.Semaphore entre todas as tarefas que usam o mesmo recurso limitado.TaskGroup por lote para limitar tarefas de processamento; o semáforo, sozinho, não limita tarefas aguardando.Parabéns! Você concluiu: Limitar operações assíncronas com Semaphore
Você concluiu este nível!
Agora você vai iniciar: Desempenho, algoritmos e memória
Estimar tempo e memória com notação O grandeEstime como o trabalho e a memória necessária crescem com o tamanho da entrada, distinguindo limites assintóticos de tempos medidos em uma máquina.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