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

  1. La idea: un sol fil que no espera aturat
  2. async def i await: corrutines
  3. L'error fundacional: cridar una corrutina no l'executa
  4. asyncio.run(): engegar el món async
  5. asyncio.sleep vs time.sleep: el pecat capital
  6. asyncio.gather: N consultes alhora, mesurades
  7. Asyncio vs fils: la taula
  8. async with i async for, breument
  9. Honestedat: quan compensa i quan no
  10. 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 programa

L'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 espero
time.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())
sequencial: 6.0 s
gather:     2.0 s
la mes barata: Distribuidora A a 19.9 €

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)? — 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 un RuntimeWarning: 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 amb await.
  • await en sèrie creient que és concurrent: await a(); await b() executa a i després b. La concurrència la crea gather (o asyncio.TaskGroup, la seva alternativa moderna), no la paraula async.
  • Cridar asyncio.run() dins d'una corrutina (o dos cops imbricats): RuntimeError. Un sol run(), a la frontera del programa — com el basicConfig de 07-05, que també es configura un sol cop a l'entrada.
  • Barrejar requests o open() 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

  1. Escriu async def baixar_portada(titol: str) -> str que simuli amb await asyncio.sleep(1) la baixada i retorni f"{titol}.webp". Baixa les portades dels quatre llibres del catàleg (L'Odissea, Hamlet, El Quixot, Faust) amb gather i comprova mesurant amb time.perf_counter() que triga ~1 s i no ~4 s.

  2. 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())
    
  3. 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

  1. 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.

  2. Dues fallades: time.sleep(2) bloqueja l'event loop (ningú no avança durant aquests 2 s), i la comprensió amb await 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
    
  3. (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

Mòdul 2: Estructures de Control

Mòdul 3: Funcions i Mòduls

Mòdul 4: Estructures de Dades

Mòdul 5: Programació Orientada a Objectes

Mòdul 6: Gestió de Fitxers

Mòdul 7: Gestió d'Errors i Excepcions

Mòdul 8: Temes Avançats

Mòdul 9: Proves i Depuració

Mòdul 10: Desenvolupament Web amb Python

Mòdul 11: Ciència de Dades amb Python

Mòdul 12: Projecte Final

© Copyright 2026. Tots els drets reservats