La lliçó anterior va acabar amb una pregunta incòmoda: els fils solapen esperes, però cada fil costa memòria i obre la porta a condicions de carrera. I si calgués gestionar 500 consultes? asyncio proposa un altre model: un sol fil que mai no espera aturat. Pensa en el millor cambrer de la cafeteria del costat de Papyrus: pren nota a la taula 1, i mentre la cuina prepara aquella comanda no es queda mirant la porta de la cuina — va a la taula 2, serveix la 3, cobra la 4, i torna a la 1 just quan el seu plat surt. Un cambrer, moltes taules, zero temps mort. Això és la programació asíncrona: les tasques declaren on esperaran (await) i un coordinador (el event loop, el bucle d'esdeveniments) aprofita cada espera per fer avançar una altra tasca.
Contingut
- La idea: un sol fil que no espera aturat
async defiawait: corrutines- L'error fundacional: cridar una corrutina no l'executa
asyncio.run(): engegar el món asyncasyncio.sleepvstime.sleep: el pecat capitalasyncio.gather: N consultes alhora, mesurades- Asyncio vs fils: la taula
async withiasync for, breument- Honestedat: quan compensa i quan no
- Tancament del mòdul 8
async def i await: corrutines
Una funció declarada amb async def és una corrutina: una funció que pot pausar-se a les seves esperes i cedir el torn. Si a 08-03 vas veure que un generador es pausa a cada yield, ja tens la intuïció exacta — les corrutines es van construir històricament sobre aquesta mateixa maquinària de pausar i reprendre:
import asyncio
async def consultar_preu(distribuidora: str, titol: str) -> tuple[str, float]:
print(f"→ consultant {titol} a {distribuidora}...")
await asyncio.sleep(2) # "aqui esperare 2 s: aprofita el torn"
preus = {"Distribuidora A": 19.90, "Distribuidora B": 21.50,
"Distribuidora C": 20.10}
print(f"← {distribuidora} respon")
return distribuidora, preus[distribuidora]await marca els punts d'espera: "m'aturo aquí; event loop, atén algú altre i desperta'm quan això estigui llest". Com a 08-05, asyncio.sleep(2) simula l'espera de xarxa d'una consulta real; continua sent la manera estàndard d'aprendre això sense servidors.
L'error fundacional: cridar-la no l'executa
Igual que llegir_vendes(ruta) a 08-03 no llegia res (retornava un generador pausat), cridar una corrutina no n'executa el cos:
resultat = consultar_preu("Distribuidora A", "Faust")
print(resultat)
# <coroutine object consultar_preu at 0x...> ← no s'ha consultat res!
# RuntimeWarning: coroutine 'consultar_preu' was never awaitedÉs *l'*error típic d'asyncio: oblidar l'await. La crida crea l'objecte corrutina; executar-la exigeix await (des d'una altra corrutina) o lliurar-la a l'event loop. Si veus RuntimeWarning: ... was never awaited al teu terminal, busca la crida sense await — sempre n'hi ha una.
asyncio.run(): engegar el món async
await només es pot escriure dins d'un async def... llavors, qui executa la primera corrutina? asyncio.run(): crea l'event loop (el cambrer), li lliura la corrutina principal i no retorna fins que tot acaba:
async def main() -> None:
dist, preu = await consultar_preu("Distribuidora A", "Faust")
print(f"{dist}: {preu} €")
asyncio.run(main()) # l'UNIC punt d'entrada; un cop per programaL'estructura estàndard d'un script async: corrutines a dalt, un main() asíncron que les orquestra, i un únic asyncio.run(main()) al final (sota if __name__ == "__main__": si és un mòdul, com sempre des de M3).
asyncio.sleep vs time.sleep: el pecat capital
Aquí hi ha la regla que separa l'asyncio que funciona del que no. L'event loop és un sol fil: si una corrutina executa alguna cosa bloquejant — time.sleep(2), un open().read() gegant, una petició HTTP síncrona — el cambrer es queda congelat en aquella taula i totes les altres tasques s'aturen amb ell.
async def consulta_MAL(distribuidora: str) -> None:
time.sleep(2) # ☠ bloqueja l'event loop SENCER: tota la resta queda aturada
async def consulta_BE(distribuidora: str) -> None:
await asyncio.sleep(2) # ✔ cedeix el torn: els altres avancen mentre esperotime.sleep(2) |
await asyncio.sleep(2) |
|
|---|---|---|
| Qui espera? | El fil sencer (el cambrer, aturat) | Només aquella corrutina (aquella taula) |
| Les altres tasques | Congelades | Avancen |
| 3 consultes "concurrents" | 6 s (seqüencial disfressat!) | 2 s |
Generalització important: tot el que facis servir dins d'asyncio ha de ser async. D'aquí que l'ecosistema tingui biblioteques paral·leles: per a HTTP real no serveix el clàssic requests (síncron, bloqueja el loop) — es fa servir aiohttp, que aquí només esmentem; les peticions reals arriben al mòdul 10.
asyncio.gather: N consultes alhora
Fer await d'una en una continua sent seqüencial (espera A, després espera B...). Per llançar diverses corrutines alhora i recollir-ne tots els resultats, asyncio.gather:
import time
async def main() -> None:
distribuidores = ["Distribuidora A", "Distribuidora B", "Distribuidora C"]
# Versio sequencial: cada await espera que l'anterior acabi
inici = time.perf_counter()
for d in distribuidores:
await consultar_preu(d, "Faust")
print(f"sequencial: {time.perf_counter() - inici:.1f} s") # ~6.0 s
# Versio gather: les tres esperes se solapen en el mateix fil
inici = time.perf_counter()
resultats = await asyncio.gather(
*(consultar_preu(d, "Faust") for d in distribuidores) # genexpr de 08-03
)
print(f"gather: {time.perf_counter() - inici:.1f} s") # ~2.0 s
dist, preu = min(resultats, key=lambda r: r[1])
print(f"la mes barata: {dist} a {preu} €")
asyncio.run(main())gather rep corrutines (aquí desempaquetades amb el * de M3 des d'una expressió generadora), les executa concurrentment i retorna la llista de resultats en l'ordre d'entrada — sense fils, sense Lock, sense condicions de carrera: entre await i await ningú no t'interromp, així que el comptador de vendes de 08-05 aquí no es corrompria. Si una corrutina llança, gather propaga l'excepció (i amb return_exceptions=True te les lliura com a valors per tractar-les una a una, a l'estil dels futurs de 08-05).
Asyncio vs fils
El mateix temps (~2 s) a l'exemple de les distribuïdores — llavors, quin fer servir?
| Criteri | Fils (ThreadPoolExecutor) |
asyncio |
|---|---|---|
| Model | Diversos fils; el SO decideix els torns | Un fil; les tasques cedeixen el torn a l'await |
| Condicions de carrera | Possibles a qualsevol línia → Lock |
Només als await (molt més predictibles) |
| Escala raonable | Desenes de tasques | Milers de tasques (una connexió = una corrutina barata) |
| Funciona amb biblioteques normals (síncrones)? | Sí — el seu gran avantatge | No: exigeix biblioteques async (aiohttp, no requests) |
| Accelera CPU pura? | No (GIL) | No (un sol fil!) — per a CPU, processos (08-05) |
| Estil de codi | Funcions normals | async/await pertot arreu (és contagiós) |
Regla pràctica: poques tasques d'E/S amb biblioteques clàssiques → fils, que s'integren sense reescriure res. Moltíssimes connexions simultànies, o un ecosistema ja async → asyncio. CPU → processos, sempre.
async with i async for
Existeixen versions asíncrones de dos vells amics: async with per a gestors de context l'entrada/sortida dels quals espera E/S (una connexió de xarxa que triga a obrir-se — el protocol és __aenter__/__aexit__, mirall de 08-04), i async for per iterar fonts que produeixen valors amb espera entre ells. En tindràs prou de reconèixer-los quan els vegis en documentació de biblioteques async; no els necessites fins que hi treballis.
Honestedat: quan compensa
Siguem clars, perquè l'async té molt de màrqueting: per a Papyrus — un script que consulta 3 distribuïdores dos cops al dia — asyncio no compensa. El ThreadPoolExecutor de 08-05 resol el mateix, amb menys conceptes nous i sense exigir biblioteques especials. Fins i tot la versió seqüencial de 6 segons és defensable: és la més simple i 6 segons dos cops al dia no fan mal (l'última fila de la taula de decisió de 08-05 continua vigent).
On asyncio brilla de debò és on els fils no arriben: servidors amb milers de connexions simultànies — mil clients esperant resposta són mil corrutines barates dormint als seus await, no mil fils devorant memòria. Per això els frameworks web moderns de Python són asíncrons per dins. Quan al mòdul 10 muntem serveis web per a Papyrus, sabràs quin motor rugeix a sota.
Errors Comuns i Consells
- Oblidar l'
await: obtens un objecte corrutina i unRuntimeWarning: coroutine ... was never awaited. La funció "no fa res". És l'error #1; ara ja en saps llegir el símptoma. time.sleep(o qualsevol crida bloquejant) dins d'una corrutina: congela l'event loop sencer i la teva "concurrència" passa a ser seqüencial disfressat. Dins d'async def, tota espera es fa ambawait.awaiten sèrie creient que és concurrent:await a(); await b()executa a i després b. La concurrència la creagather(oasyncio.TaskGroup, la seva alternativa moderna), no la paraulaasync.- Cridar
asyncio.run()dins d'una corrutina (o dos cops imbricats):RuntimeError. Un solrun(), a la frontera del programa — com elbasicConfigde 07-05, que també es configura un sol cop a l'entrada. - Barrejar
requestsoopen()pesats en corrutines: funcionen, però bloquegen el loop. Ecosistema async o fils; a mitges, no. - Consell: davant del dubte entre fils i asyncio, comença per fils. Migrar a asyncio després és assumible; forçar asyncio on no toca és reescriure-ho tot.
Exercicis
-
Escriu
async def baixar_portada(titol: str) -> strque simuli ambawait asyncio.sleep(1)la baixada i retornif"{titol}.webp". Baixa les portades dels quatre llibres del catàleg (L'Odissea, Hamlet, El Quixot, Faust) ambgatheri comprova mesurant ambtime.perf_counter()que triga ~1 s i no ~4 s. -
Aquest codi diu que triga 2 s i en triga 6. Explica per què línia a línia i corregeix-lo (dos canvis).
async def consulta(d): time.sleep(2) return d async def main(): resultats = [await consulta(d) for d in ("A", "B", "C")] asyncio.run(main()) -
Sense escriure codi: per a cada escenari de Papyrus, tria seqüencial, fils, processos o asyncio, i justifica-ho en una frase. (a) Generar les estadístiques anuals de vendes de 10 anys, càlcul intensiu. (b) El tancament de caixa diari, que triga 0.04 s. (c) Un futur servei web que atengui 2 000 llibreries del barri consultant estoc alhora.
Solucions
-
import asyncio, time async def baixar_portada(titol: str) -> str: await asyncio.sleep(1) # baixada simulada return f"{titol}.webp" async def main() -> None: titols = ["L'Odissea", "Hamlet", "El Quixot", "Faust"] inici = time.perf_counter() portades = await asyncio.gather(*(baixar_portada(t) for t in titols)) print(portades, f"{time.perf_counter() - inici:.1f} s") # [...] 1.0 s asyncio.run(main())Quatre esperes d'1 s solapades en un sol fil: total ~1 s.
-
Dues fallades:
time.sleep(2)bloqueja l'event loop (ningú no avança durant aquests 2 s), i la comprensió ambawait consulta(d)espera cada consulta abans de llançar la següent — seqüencial pur. Correcció:async def consulta(d): await asyncio.sleep(2) # canvi 1: espera async que cedeix el torn return d async def main(): resultats = await asyncio.gather(*(consulta(d) for d in ("A", "B", "C"))) # canvi 2: gather llanca les tres alhora → ~2 s -
(a) Processos (
ProcessPoolExecutor): és CPU pura i ni fils ni asyncio l'acceleren — només el paral·lelisme real de diversos GIL. (b) Seqüencial: 0.04 s no és una espera que faci mal; afegir-hi concurrència seria pagar complexitat per no res. (c) Asyncio: milers de connexions simultànies d'E/S és exactament l'escenari on les corrutines barates guanyen als fils — i el terreny dels frameworks del mòdul 10.
Conclusió
asyncio completa el mapa de la concurrència: un sol fil, corrutines que es pausen a l'await com els generadors ho feien al yield, asyncio.run() com a única porta d'entrada, la regla d'or de no bloquejar mai l'event loop (asyncio.sleep, mai time.sleep), i gather convertint 6 segons en 2 sense cap Lock. I la lliçó de maduresa: per a l'script de Papyrus n'hi ha prou amb els fils — o amb res —; l'async brilla en servidors amb milers de connexions, cita a la qual acudirem al mòdul 10.
El mòdul 8 ha complert, una a una, les promeses del tancament del mòdul 7. Els contractes que aquell mòdul defensava a mà ara estan escrits a les signatures — cercar_llibre(cataleg: dict[str, Llibre], titol: str) -> Llibre | None diu tota la veritat, i mypy la verifica sense executar res (08-01). La fontaneria repetida va sortir de les funcions i es va fer embolcall: @cronometrar mesura, @registrar_crida audita a papyrus.log i @reintentar(cops=3) insisteix, tot sense tocar una línia de vendre() (08-02). El tancament de caixa va deixar de carregar vendes.csv sencer: un pipeline de generadors — llegir → parsejar → filtrar → sumar — processa un any de vendes amb la memòria d'una sola fila (08-03). La promesa més antiga, la del with que arrossegàvem des del mòdul 6, va quedar saldada: __enter__ prepara, __exit__ neteja sempre, i TransaccioCataleg garanteix que una venda a mitges no deixi mai el catàleg corrupte (08-04). I quan les esperes van fer mal, vas aprendre a solapar-les amb fils, a paral·lelitzar càlcul amb processos i a orquestrar milers d'esperes amb un sol cambrer — sabent, sobretot, quan no cal cap de les tres (08-05, 08-06). Papyrus és ara expressiu, eficient i està preparat per créixer. Però queda una pregunta que cap type hint, decorador ni context manager no respon: quan demà canviïs una línia — un descompte nou, un camp més al CSV — com garanteixes que tot això CONTINUA funcionant? Comprovar-ho a mà cada vegada no escala. S'escriu codi que comprova el codi: proves. Aquest és el mòdul 9.
Curs de Programació en Python
Mòdul 1: Introducció a Python
- Introducció a Python
- Configuració de l'Entorn de Desenvolupament
- Sintaxi de Python i Tipus de Dades Bàsics
- Variables i Constants
- Entrada i Sortida Bàsica
- Entorns Virtuals i Gestió de Paquets
Mòdul 2: Estructures de Control
Mòdul 3: Funcions i Mòduls
- Definició de Funcions
- Arguments de Funció
- Funcions Lambda
- Mòduls i Paquets
- Visió General de la Biblioteca Estàndard
Mòdul 4: Estructures de Dades
Mòdul 5: Programació Orientada a Objectes
Mòdul 6: Gestió de Fitxers
- Lectura i Escriptura de Fitxers
- Treball amb Fitxers CSV
- Gestió de Dades JSON
- Operacions amb Fitxers i Directoris
Mòdul 7: Gestió d'Errors i Excepcions
- Introducció a les Excepcions
- Gestió d'Excepcions
- Llançament d'Excepcions
- Excepcions Personalitzades
- Bones Pràctiques i Registre d'Errors amb logging
Mòdul 8: Temes Avançats
- Anotacions de Tipus (type hints)
- Decoradors
- Generadors
- Gestors de Context
- Concurrència: Fils i Processos
- Asyncio per a la Programació Asíncrona
Mòdul 9: Proves i Depuració
- Introducció a les Proves
- Proves Unitàries amb unittest
- Proves amb pytest
- Desenvolupament Guiat per Proves
- Tècniques de Depuració
- Ús de pdb per a la Depuració
Mòdul 10: Desenvolupament Web amb Python
- Introducció al Desenvolupament Web
- Fonaments del Framework Flask
- Construcció d'APIs REST amb Flask
- Introducció a Django
- Construcció d'Aplicacions Web amb Django
Mòdul 11: Ciència de Dades amb Python
- Introducció a la Ciència de Dades
- NumPy per a la Computació Numèrica
- Pandas per a la Manipulació de Dades
- Matplotlib per a la Visualització de Dades
- Introducció a l'Aprenentatge Automàtic amb scikit-learn
