Python

Dominando asyncio Avançado em Python: Semáforos, Locks e Padrões de Concorrência em Projetos Reais

13 min de leitura

Dominando asyncio Avançado em Python: Semáforos, Locks e Padrões de Concorrência em Projetos Reais

Introdução: O Problema da Concorrência Controlada Quando trabalhamos com em Python, rapidamente nos deparamos com um desafio fundamental: múltiplas corrotinas precisam acessar o mesmo recurso compartilhado de forma segura e ordenada. Imagine um cenário onde 10 corrotinas tentam escrever em um arquivo simultaneamente, ou onde várias requisições HTTP concorrem pelo mesmo pool de conexões. Sem mecanismos de controle, isso resulta em race conditions, corrupção de dados e comportamento impredizível. Os Semáforos, Locks e padrões avançados de concorrência existem justamente para resolver esse problema. Neste artigo, você aprenderá não apenas como usá-los, mas por que cada um existe e quando aplicar cada estratégia. Vamos além dos tutoriais básicos e explorar casos reais de produção. Locks (Travas): O Fundamento da Exclusão Mútua O que é um Lock e por que precisamos dele Um Lock é o mecanismo mais simples para garantir que apenas uma corrotina execute um trecho crítico de código por vez. Pense nele como uma porta que só uma pessoa

<h2>Introdução: O Problema da Concorrência Controlada</h2>

<p>Quando trabalhamos com <code>asyncio</code> em Python, rapidamente nos deparamos com um desafio fundamental: múltiplas corrotinas precisam acessar o mesmo recurso compartilhado de forma segura e ordenada. Imagine um cenário onde 10 corrotinas tentam escrever em um arquivo simultaneamente, ou onde várias requisições HTTP concorrem pelo mesmo pool de conexões. Sem mecanismos de controle, isso resulta em race conditions, corrupção de dados e comportamento impredizível.</p>

<p>Os Semáforos, Locks e padrões avançados de concorrência existem justamente para resolver esse problema. Neste artigo, você aprenderá não apenas <em>como</em> usá-los, mas <em>por que</em> cada um existe e quando aplicar cada estratégia. Vamos além dos tutoriais básicos e explorar casos reais de produção.</p>

<h2>Locks (Travas): O Fundamento da Exclusão Mútua</h2>

<h3>O que é um Lock e por que precisamos dele</h3>

<p>Um Lock é o mecanismo mais simples para garantir que apenas uma corrotina execute um trecho crítico de código por vez. Pense nele como uma porta que só uma pessoa pode abrir ao mesmo tempo. Se outra corrotina tentar entrar enquanto a porta está fechada, ela aguarda sua vez.</p>

<p>Em <code>asyncio</code>, um Lock é criado e gerenciado através da classe <code>asyncio.Lock</code>. A sintaxe é clara: você adquire o lock, executa seu código crítico e libera o lock. Se não liberar (ou se ocorrer uma exceção), você pode travar toda a sua aplicação. Por isso, sempre use a declaração <code>async with</code>.</p>

<pre><code class="language-python">import asyncio

Simulando um recurso compartilhado (contador)

contador = 0

lock = asyncio.Lock()

async def incrementar():

global contador

SEM LOCK - PERIGOSO (NÃO FAÇA ASSIM)

temp = contador

await asyncio.sleep(0.001)

contador = temp + 1

COM LOCK - SEGURO

async with lock:

temp = contador

await asyncio.sleep(0.001) # Simula uma operação demorada

contador = temp + 1

async def main():

global contador

contador = 0

100 corrotinas tentando incrementar simultaneamente

await asyncio.gather(*[incrementar() for _ in range(100)])

print(f&quot;Contador final: {contador}&quot;)

print(f&quot;Esperado: 100&quot;)

asyncio.run(main())</code></pre>

<p><strong>Saída esperada:</strong></p>

<pre><code>Contador final: 100

Esperado: 100</code></pre>

<p>Observe que usamos <code>async with lock:</code> para adquirir e liberar automaticamente. Isso é essencial para evitar deadlocks mesmo em caso de exceção. Se você usar <code>await lock.acquire()</code> manualmente, <strong>nunca esqueça</strong> do correspondente <code>lock.release()</code>.</p>

<h3>Quando um Lock não é suficiente</h3>

<p>Locks são binários: ou você tem acesso, ou não tem. Mas e se você quer permitir que 5 corrotinas acessem um recurso simultaneamente, mas bloquear a 6ª? Para isso, precisamos de Semáforos.</p>

<h2>Semáforos: Controlando o Acesso em Múltiplas Instâncias</h2>

<h3>Entendendo a Semântica de Semáforos</h3>

<p>Um Semáforo é um contador que começa com um valor inicial (digamos, 5). Quando uma corrotina tenta acessar o recurso, o contador decrementa. Se o contador chegar a 0, novas corrotinas aguardam até que algo libere acesso (incrementando o contador novamente). Isso é perfeito para limitar concorrência em pools de recursos.</p>

<pre><code class="language-python">import asyncio

import time

Semáforo que permite no máximo 3 corrotinas simultâneas

semaforo = asyncio.Semaphore(3)

async def trabalho_com_limite(id_tarefa):

async with semaforo:

print(f&quot;[{time.time():.2f}] Tarefa {id_tarefa} iniciada&quot;)

await asyncio.sleep(2) # Simula trabalho demorado

print(f&quot;[{time.time():.2f}] Tarefa {id_tarefa} concluída&quot;)

async def main():

Cria 10 tarefas, mas apenas 3 executam simultaneamente

inicio = time.time()

await asyncio.gather(*[trabalho_com_limite(i) for i in range(10)])

tempo_total = time.time() - inicio

print(f&quot;\nTempo total: {tempo_total:.2f}s&quot;)

print(f&quot;Esperado: ~7s (10 tarefas / 3 em paralelo = 4 rodadas, 3 esperas)&quot;)

asyncio.run(main())</code></pre>

<p><strong>Saída esperada (timestamps relativos):</strong></p>

<pre><code>[0.00] Tarefa 0 iniciada

[0.00] Tarefa 1 iniciada

[0.00] Tarefa 2 iniciada

[2.00] Tarefa 0 concluída

[2.00] Tarefa 3 iniciada

[2.00] Tarefa 1 concluída

[2.00] Tarefa 4 iniciada

...

Tempo total: 7.00s

Esperado: ~7s</code></pre>

<p>A grande diferença entre Lock e Semáforo é que o Lock é sempre binário (1 acesso por vez), enquanto o Semáforo é configurável. Use Semáforo quando quiser controlar um número máximo de acessos simultâneos.</p>

<h3>BoundedSemaphore: Proteção contra Erros</h3>

<p>Existe uma variante chamada <code>BoundedSemaphore</code> que previne que você libere mais acesso do que o limite inicial. Isso protege contra bugs onde você chama <code>release()</code> sem correspondente <code>acquire()</code>.</p>

<pre><code class="language-python">import asyncio

BoundedSemaphore com limite de 2

semaforo = asyncio.BoundedSemaphore(2)

async def exemplo_error():

await semaforo.acquire()

print(f&quot;Adquirido. Valor interno: {semaforo._value}&quot;)

Tentar liberar mais vezes que o limite

semaforo.release()

print(f&quot;Liberado 1x. Valor interno: {semaforo._value}&quot;)

try:

semaforo.release() # Aqui vai dar erro

except ValueError as e:

print(f&quot;Erro capturado: {e}&quot;)

asyncio.run(exemplo_error())</code></pre>

<h2>Padrões Avançados de Concorrência</h2>

<h3>Condition: Sincronização com Predicados</h3>

<p>Um <code>Condition</code> combina um Lock com um mecanismo de notificação. É útil quando uma corrotina precisa esperar por uma condição específica ser satisfeita por outra corrotina. Diferente de um Lock puro, você pode &quot;acordar&quot; seletivamente corrotinas que estão aguardando.</p>

<pre><code class="language-python">import asyncio

Simulando um produtor-consumidor

condicao = asyncio.Condition()

buffer = []

async def produtor():

&quot;&quot;&quot;Adiciona itens ao buffer e notifica consumidores&quot;&quot;&quot;

for i in range(5):

async with condicao:

buffer.append(f&quot;item-{i}&quot;)

print(f&quot;Produzido: item-{i}, buffer agora: {buffer}&quot;)

condicao.notify_all() # Acorda TODOS os consumidores

await asyncio.sleep(0.5)

async def consumidor(id_consumidor):

&quot;&quot;&quot;Aguarda itens no buffer&quot;&quot;&quot;

while True:

async with condicao:

Aguarda enquanto buffer estiver vazio

await condicao.wait_for(lambda: len(buffer) &gt; 0)

item = buffer.pop(0)

print(f&quot; Consumidor-{id_consumidor} consumiu: {item}&quot;)

Se foi o último item, acordar produtores se houvesse

if len(buffer) == 0:

print(f&quot; Consumidor-{id_consumidor}: buffer vazio&quot;)

await asyncio.sleep(0.1)

Saída artificial para não rodar infinitamente

if id_consumidor == 0 and len(buffer) == 0:

break

async def main():

Cria produtor e 2 consumidores

prod = asyncio.create_task(produtor())

cons1 = asyncio.create_task(consumidor(1))

cons2 = asyncio.create_task(consumidor(2))

await prod

Aguarda consumidores finalizarem

await asyncio.sleep(2)

asyncio.run(main())</code></pre>

<p><strong>Saída esperada:</strong></p>

<pre><code>Produzido: item-0, buffer agora: [&#039;item-0&#039;]

Consumidor-1 consumiu: item-0

Consumidor-1: buffer vazio

Produzido: item-1, buffer agora: [&#039;item-1&#039;]

Consumidor-2 consumiu: item-1

...</code></pre>

<h3>Event: Sinalização Simples</h3>

<p>Um <code>Event</code> é ainda mais simples que um Condition. É um booleano que pode ser &quot;setado&quot; (True) ou &quot;limpo&quot; (False). As corrotinas podem aguardar até que o evento seja setado.</p>

<pre><code class="language-python">import asyncio

evento_inicio = asyncio.Event()

async def trabalhador(id_trabalhador):

print(f&quot;Trabalhador-{id_trabalhador} aguardando sinal...&quot;)

await evento_inicio.wait() # Bloqueia até que o evento seja setado

print(f&quot;Trabalhador-{id_trabalhador} começando trabalho!&quot;)

await asyncio.sleep(1)

print(f&quot;Trabalhador-{id_trabalhador} concluído!&quot;)

async def coordinador():

await asyncio.sleep(2)

print(&quot;Coordinador: iniciando todos os trabalhadores!&quot;)

evento_inicio.set() # Todos os aguardadores são desbloqueados

async def main():

3 trabalhadores esperando

trabalhadores = [asyncio.create_task(trabalhador(i)) for i in range(3)]

coord = asyncio.create_task(coordinador())

await asyncio.gather(*trabalhadores, coord)

asyncio.run(main())</code></pre>

<p><strong>Saída esperada:</strong></p>

<pre><code>Trabalhador-0 aguardando sinal...

Trabalhador-1 aguardando sinal...

Trabalhador-2 aguardando sinal...

Coordinador: iniciando todos os trabalhadores!

Trabalhador-0 começando trabalho!

Trabalhador-1 começando trabalho!

Trabalhador-2 começando trabalho!

...</code></pre>

<h2>Padrão Prático: Rate Limiting em Requisições HTTP</h2>

<p>Um dos casos de uso mais comuns é limitar a taxa de requisições para não sobrecarregar uma API externa. Vamos combinar Semáforo com requisições reais:</p>

<pre><code class="language-python">import asyncio

import aiohttp

class RateLimiter:

&quot;&quot;&quot;

Limita a taxa de requisições usando Semáforo.

Garante que apenas max_concurrent requisições aconteçam simultaneamente.

&quot;&quot;&quot;

def __init__(self, max_concurrent=5):

self.semaforo = asyncio.Semaphore(max_concurrent)

self.session = None

async def fetch(self, url):

async with self.semaforo:

async with self.session.get(url, timeout=5) as response:

return await response.text()

async def __aenter__(self):

self.session = aiohttp.ClientSession()

return self

async def __aexit__(self, *args):

await self.session.close()

async def main():

urls = [

&quot;https://httpbin.org/delay/1&quot;,

&quot;https://httpbin.org/delay/1&quot;,

&quot;https://httpbin.org/delay/1&quot;,

&quot;https://httpbin.org/delay/1&quot;,

&quot;https://httpbin.org/delay/1&quot;,

]

async with RateLimiter(max_concurrent=2) as limiter:

Mesmo com 5 URLs, apenas 2 requisições acontecem por vez

tasks = [limiter.fetch(url) for url in urls]

results = await asyncio.gather(*tasks, return_exceptions=True)

print(f&quot;Completadas {len([r for r in results if r])} requisições&quot;)

asyncio.run(main())

Nota: Descomente para testar com aiohttp instalado</code></pre>

<p>Este padrão é fundamental em produção para evitar que sua aplicação seja banida por fazer muitas requisições simultâneas.</p>

<h2>Antipadrões Comuns e Como Evitá-los</h2>

<h3>1. Deadlock: Adquirir locks em ordem diferente</h3>

<pre><code class="language-python">import asyncio

lock_a = asyncio.Lock()

lock_b = asyncio.Lock()

async def tarefa_1():

async with lock_a:

await asyncio.sleep(0.1)

async with lock_b:

print(&quot;Tarefa 1 completada&quot;)

async def tarefa_2():

async with lock_b:

await asyncio.sleep(0.1)

async with lock_a:

print(&quot;Tarefa 2 completada&quot;)

Isso causará DEADLOCK!

asyncio.run(asyncio.gather(tarefa_1(), tarefa_2()))

SOLUÇÃO: Sempre adquirir na mesma ordem

async def tarefa_1_corrigida():

async with lock_a:

await asyncio.sleep(0.1)

async with lock_b:

print(&quot;Tarefa 1 completada&quot;)

async def tarefa_2_corrigida():

async with lock_a: # Mesma ordem!

await asyncio.sleep(0.1)

async with lock_b:

print(&quot;Tarefa 2 completada&quot;)

asyncio.run(asyncio.gather(tarefa_1_corrigida(), tarefa_2_corrigida()))</code></pre>

<h3>2. Não liberar o lock em caso de exceção</h3>

<pre><code class="language-python">import asyncio

lock = asyncio.Lock()

contador = 0

async def perigoso():

&quot;&quot;&quot;NÃO FAÇA ASSIM&quot;&quot;&quot;

await lock.acquire()

try:

contador += 1

raise ValueError(&quot;Ops!&quot;)

finally:

lock.release() # Nem sempre chamado se esquecer try/finally

async def seguro():

&quot;&quot;&quot;FAÇA ASSIM&quot;&quot;&quot;

async with lock:

contador += 1

raise ValueError(&quot;Ops!&quot;)

Lock é liberado automaticamente mesmo com exceção</code></pre>

<h2>Conclusão</h2>

<p>Você aprendeu que <strong>locks são binários e ideais para seções críticas pequenas</strong>, enquanto <strong>semáforos controlam concorrência em múltiplas instâncias de um recurso</strong>. Além disso, <strong>Condition e Event resolvem problemas de sincronização entre corrotinas</strong>, permitindo comunicação eficiente sem busy-waiting.</p>

<p>O padrão prático que você deve levar: sempre use <code>async with</code> para adquirir locks e semáforos, nunca misture ordens de aquisição de múltiplos locks, e considere usar Semáforo quando quiser limitar concorrência em APIs externas ou recursos finitos.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://docs.python.org/3/library/asyncio-sync.html#asyncio.Lock" target="_blank" rel="noopener noreferrer">Documentação oficial asyncio.Lock</a></li>

<li><a href="https://docs.python.org/3/library/asyncio-sync.html#asyncio.Semaphore" target="_blank" rel="noopener noreferrer">Documentação oficial asyncio.Semaphore</a></li>

<li><a href="https://realpython.com/async-io-python/#the-asyncio-module" target="_blank" rel="noopener noreferrer">Real Python: asyncio Synchronization Primitives</a></li>

<li><a href="https://docs.python.org/3/library/asyncio-sync.html#asyncio.Condition" target="_blank" rel="noopener noreferrer">Python asyncio.Condition Documentation</a></li>

<li><a href="https://www.oreilly.com/library/view/fluent-python-2nd/9781492056348/" target="_blank" rel="noopener noreferrer">Fluent Python - Luciano Ramalho (Capítulo sobre asyncio)</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em Python

pip, virtualenv e venv em Python: Isolamento de Dependências: Do Básico ao Avançado
pip, virtualenv e venv em Python: Isolamento de Dependências: Do Básico ao Avançado

O Problema do Caos de Dependências Quando começamos a trabalhar com Python, e...

SQLAlchemy Core em Python: Conexão, Queries e Transactions na Prática
SQLAlchemy Core em Python: Conexão, Queries e Transactions na Prática

Introdução ao SQLAlchemy Core SQLAlchemy é a biblioteca SQL mais madura e rob...

Ruff, Black e isort em Python: Linting e Formatação Automatizada: Do Básico ao Avançado
Ruff, Black e isort em Python: Linting e Formatação Automatizada: Do Básico ao Avançado

Introdução: Por que Qualidade de Código Importa Quando você começa a programa...