Aiuto con asyncio in Python: loop bloccato su operazione I/O, soluzioni?

👤 Iniziato da @mauriziosorrentino30
📅 22/06/2025 07:20
📁 Programmazione 🌐 IT
Avatar di mauriziosorrentino30
Ciao a tutti, sto lavorando a un bot Telegram con Python 3.11 usando asyncio, ma mi sono imbattuto in un problema frustrante. Quando eseguo operazioni I/O pesanti (tipo scaricare file grandi o query di database lunghe), l'event loop si blocca completamente invece di rimanere reattivo. Ho provato a usare loop.run_in_executor() per spostare il carico su thread separati, ma a volte ottengo race condition. Ecco un estratto del codice problematico:

```python
async def download_file(url):
# Blocca il loop per 10+ secondi con file grossi
data = requests.get(url).content
# ...
```
Secondo voi è meglio riprogettare tutto con thread dedicati o ci sono pattern asyncio che non conosco? Avete esperienze simili con librerie come aiohttp per I/O non bloccante? Grazie mille per qualsiasi suggerimento!
Avatar di oliviafabbri69
La radice del problema è usare `requests` in un contesto async: è una libreria sincrona e blocca l'event loop. La soluzione corretta è sostituirla con una lib async come `aiohttp`. Ecco come potresti riscrivere la funzione:

```python
async def download_file(url):
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
data = await response.read()
```

`run_in_executor` funziona ma crea overhead e problemi di sincronizzazione (race condition) se non gestisci i dati con attenzione. Per le query database, devi usare driver asyncroni (es. `asyncpg` per PostgreSQL o `motor` per MongoDB), non librerie sincrone.

Se non trovi alternative async per qualche libreria, puoi isolare le operazioni bloccanti in un thread dedicato con `loop.run_in_executor(None, ...)` ma limita il numero di thread attivi e usa lock per risorse condivise. Non riscrivere tutto in threading: asyncio è più leggero e adatto ai bot Telegram che devono gestire migliaia di connessioni simultanee. La prima cosa: sostituisci requests con aiohttp e vedi se la situazione migliora.
Avatar di volfangomonti8
Maurizio, il problema è chiaro: stai usando `requests` che è sincrono dentro asyncio. È come cercare di correre una maratona con i tacchi a spillo. Olivia ha centrato il punto: **aiohttp** è la via maestra per le richieste HTTP non bloccanti. Riscrivi la funzione come ti ha suggerito, ma aggiungo due cose cruciali:

1. **Timeout e gestione errori**:
```python
try:
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=30)) as session:
async with session.get(url) as response:
response.raise_for_status()
data = await response.read()
except (aiohttp.ClientError, asyncio.TimeoutError) as e:
# Logga l'errore o gestisci il fallimento
```
Senza timeout, operazioni lente possono bloccare tutto ugualmente.

2. **Per il database**:
Se usi driver sincroni tipo `psycopg2`, sei punto a capo. Passa a `asyncpg` o `aiopg`. `run_in_executor` è un palliativo che introduce complessità inutile, specialmente con dati condivisi.

3. **Ottimizza le risorse**:
Non creare una nuova `ClientSession` per ogni richiesta. Riusane una globale (ma attento al ciclo di vita!).

Se proprio devi usare librerie bloccanti (tipo alcuni driver SQL legacy), incapsula *solo quelle* in `run_in_executor` con un **BoundedSemaphore** per limitare i thread attivi ed evitare il sovraccarico. Ma è un'architettura zoppa: meglio investire in librerie async native.

*(PS: Se vedi ancora blocchi, controlla se per caso stai chiamando funzioni sync senza executor. Un solo `.join()` sincrono e saluti l'event loop.)*
Avatar di reefcaputo
Maurizio, non è un problema di asyncio ma di architettura: usare `requests` in async è come cercare di far nuotare un gatto dentro un secchio. Se il loop si blocca, non è "non bloccante" ma un finto bot che finge di essere reattivo. La soluzione è obbligata: sostituisci **ogni** operazione sincrona con risorse async-native.
Per gli scaricamenti, `aiohttp` è la scelta giusta ma aggiungi anche il `content.read()` in streaming per evitare di caricare tutto in memoria:
```python
async with session.get(url) as response:
async for chunk in response.content.iter_chunked(64*1024): # 64KB
await process_chunk(chunk) # o scrivi su file
```
Per il database, se sei su SQLite (che non ha driver async decenti), sposta le query su un thread dedicato con un `ThreadPoolExecutor` isolato, non nell'event loop principale. Usa `asyncio.to_thread()` da Python 3.9+ per incapsulare l'operazione sincrona in modo più pulito rispetto a `run_in_executor`. Le race condition nascono perché probabilmente stai accedendo a variabili condivise senza lock: proteggi gli accessi critici o, meglio, riscrivi il codice per minimizzare lo stato globale.
Evita di mischiare thread e asyncio "alla cazzo" (sistema a parte se sei su Windows, dove certe librerie async funzionano male per limiti OS). Se proprio devi usare i thread, fai un modulo isolato che gestisce la sincronizzazione con code (`asyncio.Queue`) e life on. Ma il consiglio di Olivia e Volfango è oro: asyncio + librerie async è la combo per scalare senza impazzire.
Avatar di adalbertosala40
Maurizio, la sostanza è questa: **`requests` e asyncio non vanno d'accordo**. Se insisti a usarli insieme, finisci per pagare overhead senza benefici. Ti do un esempio pratico: quando scarichi un file da 1GB con `requests.get()`, anche se lo metti in un executor, quel thread finisce bloccato in attesa del download e non puoi controllare il timeout fine. Con `aiohttp`, invece, puoi fare streaming chunk per chunk, come ha detto Reef, e rilasciare il loop dopo ogni blocco.

Per il database, se sei su PostgreSQL, **`asyncpg` è un must**. Non solo è non-bloccante, ma gestisce anche i risultati in modo asincrono senza lock vari. Con SQLite, invece, sei in un territorio minato: non ha driver async solidi, quindi devi isolare le operazioni in un thread dedicato e usare una coda (`asyncio.Queue`) per evitare conflitti.

Un consiglio spiccio: **non mescolare executor e asyncpg**. Se vai su asyncpg, tutte le query sono async-native. Se hai una libreria sincrona che non puoi sostituire, allora usa `asyncio.to_thread()` per isolarla, ma attenzione alla concorrenza: limita il numero di worker con un `Semaphore`.

PS: Ho avuto lo stesso problema con un bot per immagini. Passando a `aiohttp` e `asyncpg`, la latenza delle risposte è scesa da 800ms a 40ms. Fa la differenza.
Avatar di mauriziosorrentino30
Adalberto, chiarissimo! Grazie per la spiegazione definitiva su `requests` vs `aiohttp` e la conferma su `asyncpg` per PostgreSQL. Il caso del bot immagini (800ms → 40ms) è folle, mi hai convinto a rifare tutto in native async.
Per SQLite seguirò il tuo consiglio: isolamento in thread + coda e `Semaphore` per controllare la concorrenza. Terrò *assolutamente* separati executor e asyncpg, evitando mix pericolosi.
Il punto sul timeout fine durante download lunghi è oro: streaming chunk per chunk con `aiohttp` risolve proprio il mio blocco. Direi che il percorso è ora cristallino!

La Tua Risposta

💬

Vuoi partecipare alla discussione?

Accedi o registrati per scrivere la tua risposta e unirti alla conversazione!