Tot el codi de BiblioTech escrit fins ara és síncron: cada línia espera que l'anterior acabi abans d'executar-se, i si una operació triga (una crida de xarxa, una lectura de disc, una consulta a una base de dades), el fil que l'executa es queda bloquejat, sense poder fer res més, fins que acaba. En una aplicació real —una API web que atén molts socis de BiblioTech alhora, o una aplicació d'escriptori que no s'ha de "congelar" mentre carrega dades— aquest bloqueig té un cost real. La programació asíncrona, amb les paraules clau async i await, permet que un mètode cedeixi el control mentre espera una operació lenta, sense bloquejar el fil, i el recuperi automàticament quan aquesta operació acaba. Aquesta lliçó introdueix Task, async/await i el tractament d'excepcions en codi asíncron, i afegeix a Biblioteca un primer mètode PrestarLlibreAsync que simula una operació d'E/S lenta, preparant el terreny per al Mòdul 5, on aquesta simulació es convertirà en E/S real.

Contingut

  1. Codi síncron enfront de codi asíncron: per què importa
  2. Task i Task<T>: representant una operació en curs
  3. async i await: com s'escriu un mètode asíncron
  4. Simulant una operació lenta amb Task.Delay
  5. Tractament d'excepcions en mètodes async
  6. Diverses tasques en paral·lel amb Task.WhenAll
  7. PrestarLlibreAsync a Biblioteca i una crida des d'un Main asíncron

  1. Codi síncron enfront de codi asíncron: per què importa

Imagina't que Biblioteca hagués de consultar, abans de completar un préstec, un servei extern que verifica si el soci té sancions pendents. Aquesta consulta —per xarxa, o contra una base de dades— pot trigar un segon o més. Amb codi síncron, el fil que atén aquesta petició es queda completament aturat durant aquest segon:

// Sincron: el fil es bloqueja per complet durant l'espera
void PrestarLlibre(Llibre llibre, Soci soci)
{
    VerificarSancionsPendents(soci); // triga 1 segon; el fil no pot fer res mes mentrestant
    llibre.Prestar();
}

En una aplicació de consola petita, aquest segon de bloqueig amb prou feines es nota. Però en un servidor web que atén centenars de socis simultàniament, cada fil bloquejat esperant una verificació és un fil que no pot atendre cap altre soci mentrestant: el servidor necessitaria centenars de fils només per estar "esperant", la majoria d'ells sense fer cap treball útil.

Amb codi asíncron, el fil s'allibera durant l'espera i pot atendre altres tasques; tan bon punt l'operació lenta acaba, l'execució del mètode continua automàticament, sense que el programador hagi de gestionar aquest "reenganxament" manualment:

Codi síncron Codi asíncron
Durant una operació lenta (E/S) El fil es bloqueja, esperant sense fer res més El fil queda lliure per a altres tasques
Paraula clau Cap d'especial async a la declaració, await a la crida
Tipus de retorn habitual void, T Task, Task<T>
Quan aporta valor Càlcul pur en memòria (poca o cap espera) Operacions d'E/S: xarxa, disc, base de dades

És important no confondre asíncron amb "més ràpid": una operació asíncrona no es completa abans pel fet de ser-ho (Task.Delay(1000) continua trigant un segon). El que canvia és que, mentre s'espera, el fil queda disponible per fer un altre treball en comptes de quedar bloquejat sense cap utilitat.

  1. Task i Task<T>: representant una operació en curs

Un mètode asíncron no retorna directament el seu resultat, sinó un objecte Task (si no retorna cap valor útil, l'equivalent asíncron de void) o Task<T> (si en algun moment produirà un valor de tipus T). Un Task representa una operació que pot no haver acabat encara: és una mena de "rebut" que es pot consultar més endavant per obtenir el resultat, un cop estigui disponible.

Tipus Equivalent síncron Quan es fa servir
Task void El mètode asíncron no produeix cap valor de retorn
Task<T> T El mètode asíncron produirà, eventualment, un valor de tipus T
Task<int> tascaAmbResultat = ObtenirQuantitatSocisAsync(); // la tasca pot continuar en curs aqui
int quantitatSocis = await tascaAmbResultat; // "await" espera que acabi i desembolica el resultat

await tasca fa dues coses alhora: espera (sense bloquejar el fil) que la Task acabi, i "desembolica" el seu resultat —de Task<int> s'obté directament un int, no un Task<int> embolicat. Sobre una Task (sense resultat), await simplement espera que acabi, sense produir cap valor.

  1. async i await: com s'escriu un mètode asíncron

Un mètode es declara asíncron afegint el modificador async a la seva signatura, i retornant Task o Task<T> en lloc de void o T. Dins del seu cos, await es fa servir per esperar el resultat d'una altra operació asíncrona:

async Task<string> ObtenirSalutacioAsync()
{
    await Task.Delay(500); // simula una mica de treball asincron (mes detall al seguent apartat)
    return "Hola des d'un metode asincron";
}

Tres regles sintàctiques que convé fixar des del principi:

  • El modificador async va abans del tipus de retorn, igual que public o static.
  • El tipus de retorn declarat és Task o Task<T>; dins del cos, un return valor; s'escriu igual que en un mètode síncron que retornés T directament —el compilador s'encarrega d'embolicar-lo al Task<T>.
  • Per convenció, tot mètode asíncron acaba el seu nom amb el sufix Async (ObtenirSalutacioAsync, i més endavant PrestarLlibreAsync), perquè qui el crida sàpiga, només pel nom, que s'ha de fer servir amb await.

Per cridar un mètode async, es fa servir await davant de la crida, i el mètode que crida ha de ser, al seu torn, també async:

async Task MostrarSalutacioAsync()
{
    string salutacio = await ObtenirSalutacioAsync(); // espera el resultat sense bloquejar el fil
    Console.WriteLine(salutacio);
}

Aquesta propagació d'async/await cap amunt —qualsevol mètode que faci servir await ha de ser async, i qui el crida probablement també— es coneix com "async tot el camí" (async all the way), i és la raó per la qual Main necessita convertir-se en asíncron per poder cridar PrestarLlibreAsync (apartat 7).

  1. Simulant una operació lenta amb Task.Delay

Task.Delay(milisegons) retorna una Task que acaba, sense fer cap treball real, després del nombre de mil·lisegons indicat. És l'eina habitual per simular una operació lenta (una crida de xarxa, una consulta a una base de dades) en exemples i proves, sense dependre encara de cap sistema extern real:

async Task SimularOperacioLentaAsync()
{
    Console.WriteLine("Comença l'operacio...");
    await Task.Delay(2000); // simula 2 segons d'espera, com una crida de xarxa real
    Console.WriteLine("L'operacio ha acabat.");
}

Aquest és exactament el paper que Task.Delay complirà a PrestarLlibreAsync (apartat 7): en aquesta lliçó encara no hi ha cap fitxer, base de dades ni servei real a consultar —això arriba al Mòdul 5—, però Task.Delay permet escriure i provar ja el flux asíncron complet, amb el mateix async/await que es farà servir després amb E/S real.

  1. Tractament d'excepcions en mètodes async

Un dels avantatges d'async/await enfront d'alternatives més antigues de programació asíncrona és que les excepcions es tracten exactament igual que en codi síncron: un try/catch normal al voltant d'un await captura qualsevol excepció que llanci l'operació esperada, com si la crida fos síncrona:

async Task RealitzarOperacioAmbRiscAsync()
{
    await Task.Delay(500);
    throw new InvalidOperationException("Alguna cosa ha anat malament durant l'operacio.");
}

async Task GestionarOperacioAsync()
{
    try
    {
        await RealitzarOperacioAmbRiscAsync();
        Console.WriteLine("Operacio completada sense errors.");
    }
    catch (InvalidOperationException ex)
    {
        Console.WriteLine($"Error controlat: {ex.Message}");
    }
}

El try/catch embolica l'await, no la crida al mètode en si: l'excepció es propaga de forma natural des de dins de la Task fins al punt on s'espera el seu resultat, tal com passaria amb una excepció llançada per una crida síncrona directa. No cal cap sintaxi especial addicional per a "excepcions asíncrones": són excepcions normals, tractades amb les eines que ja coneixes de la lliçó de Gestió d'Excepcions (Mòdul 2).

  1. Diverses tasques en paral·lel amb Task.WhenAll

Quan hi ha diverses operacions asíncrones independents entre si (no necessita el resultat d'una per començar l'altra), esperar-les una darrere l'altra amb await malbarata l'avantatge de l'asincronia: si cadascuna triga un segon, esperar-les en seqüència triga uns dos segons en total, encara que cap depengui de l'altra.

// Seqüencial: primer espera la primera tasca sencera, despres la segona -> uns 2 segons en total
await SimularOperacioLentaAsync();
await SimularOperacioLentaAsync();

// En paral·lel: totes dues tasques s'inicien alhora i s'espera que totes dues acabin -> uns 1 segon en total
Task tasca1 = SimularOperacioLentaAsync();
Task tasca2 = SimularOperacioLentaAsync();
await Task.WhenAll(tasca1, tasca2);

Task.WhenAll rep una o diverses Task ja iniciades (nota que es crida el mètode sense await per obtenir el Task sense esperar-lo encara) i retorna una única Task que acaba quan totes han acabat. És l'eina habitual quan cal llançar diverses operacions independents —per exemple, diversos préstecs alhora— i esperar que totes es completin abans de continuar. Aquesta lliçó només presenta Task.WhenAll de forma introductòria; el Mòdul 6 (Multifil i Programació Paral·lela) aprofundeix en l'execució concurrent real amb Thread i Parallel, un escenari diferent del que resol async/await (pensat, sobretot, per no bloquejar un fil mentre s'espera E/S, no per repartir càlcul entre diversos nuclis de CPU).

  1. PrestarLlibreAsync a Biblioteca i una crida des d'un Main asíncron

Amb tot l'anterior, s'afegeix a Biblioteca una versió asíncrona d'un préstec, que simula —amb Task.Delay— una comprovació lenta (per exemple, contra un futur servei de sancions) abans de completar el préstec, reutilitzant la lògica ja existent de Prestar() i RegistrarPrestec():

class Biblioteca
{
    public List<MaterialBibliotecari> Cataleg { get; } = new List<MaterialBibliotecari>();
    public List<Soci> Socis { get; } = new List<Soci>();
    public List<Prestec> Prestecs { get; } = new List<Prestec>();

    private Dictionary<int, Soci> _socisPerId = new Dictionary<int, Soci>();

    public event Action<Prestec> PrestecRegistrat;

    // ... AfegirMaterial, AfegirSoci, CercarSociPerId, RegistrarPrestec sense canvis ...

    public async Task PrestarLlibreAsync(Llibre llibre, Soci soci)
    {
        Console.WriteLine($"Comprovant disponibilitat de '{llibre.Titol}'...");
        await Task.Delay(1000); // simula una verificacio lenta; al Modul 5 sera E/S real

        if (!llibre.Disponible)
        {
            throw new InvalidOperationException($"'{llibre.Titol}' no esta disponible per a prestec.");
        }

        llibre.Prestar();
        Prestec prestec = new Prestec(llibre, soci);
        RegistrarPrestec(prestec);
    }
}

Cap membre anterior de Biblioteca canvia: PrestarLlibreAsync és un mètode nou que reutilitza llibre.Prestar() (que actualitza Disponible i ja sap informar per consola) i RegistrarPrestec(prestec) (que afegeix el préstec a Prestecs i dispara PrestecRegistrat), embolicant aquesta lògica ja coneguda en una espera asíncrona simulada. La comprovació if (!llibre.Disponible) es fa després de l'await Task.Delay(...), a propòsit: representa que la disponibilitat es reverifica just abans de confirmar el préstec, després de la "consulta lenta" simulada, i no abans.

Per cridar PrestarLlibreAsync cal un punt d'entrada asíncron. Des de C# 7.1, Main es pot declarar static async Task Main() (o static async Task Main(string[] args)):

class Program
{
    static async Task Main()
    {
        Biblioteca biblioteca = new Biblioteca();
        biblioteca.PrestecRegistrat += prestec =>
            Console.WriteLine($"S'ha prestat '{prestec.Llibre.Titol}' a {prestec.Soci.Nom}");

        Llibre llibre1 = new Llibre("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
        Soci soci1 = new Soci(1, "Anna Martinez");
        biblioteca.AfegirMaterial(llibre1);
        biblioteca.AfegirSoci(soci1);

        try
        {
            await biblioteca.PrestarLlibreAsync(llibre1, soci1);
            Console.WriteLine("Prestec completat correctament.");
        }
        catch (InvalidOperationException ex)
        {
            Console.WriteLine($"No s'ha pogut completar el prestec: {ex.Message}");
        }
    }
}

Si el projecte fa servir top-level statements (l'estil de fitxer únic vist des de la lliçó d'Hola Món, Mòdul 1), no cal declarar Main explícitament: n'hi ha prou d'escriure await directament al fitxer, i el compilador genera un Main asíncron per darrere:

// Program.cs, amb top-level statements
Biblioteca biblioteca = new Biblioteca();
Llibre llibre1 = new Llibre("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
Soci soci1 = new Soci(1, "Anna Martinez");
biblioteca.AfegirMaterial(llibre1);
biblioteca.AfegirSoci(soci1);

await biblioteca.PrestarLlibreAsync(llibre1, soci1);
Console.WriteLine("Prestec completat correctament.");

I, retomant Task.WhenAll de l'apartat anterior, així es prestarien dos llibres alhora en lloc d'un darrere l'altre:

Llibre llibre2 = new Llibre("Ficcions", "Jorge Luis Borges", "978-84-376-0496-1");
Soci soci2 = new Soci(2, "Lluis Gomez");
biblioteca.AfegirMaterial(llibre2);
biblioteca.AfegirSoci(soci2);

Task prestec1 = biblioteca.PrestarLlibreAsync(llibre1, soci1);
Task prestec2 = biblioteca.PrestarLlibreAsync(llibre2, soci2);
await Task.WhenAll(prestec1, prestec2); // totes dues verificacions d'1 segon transcorren alhora
sequenceDiagram
    participant Main as Main (async)
    participant Bib as Biblioteca
    participant Llibre as Llibre

    Main->>Bib: await PrestarLlibreAsync(llibre1, soci1)
    Bib->>Bib: await Task.Delay(1000)
    Note over Bib: el fil queda lliure durant l'espera
    Bib->>Llibre: Disponible?
    Llibre-->>Bib: true
    Bib->>Llibre: Prestar()
    Bib->>Bib: RegistrarPrestec(prestec)
    Bib-->>Main: Task completada

Errors Comuns i Consells

  • async void en qualsevol lloc tret de gestors d'esdeveniments: un mètode async void no retorna cap Task, així que qui el crida no pot fer servir await sobre ell ni capturar les seves excepcions amb un try/catch normal —una excepció no controlada dins d'un async void pot arribar a tombar el procés. Fes servir sempre async Task (o async Task<T>); async void es reserva, exclusivament, per a gestors d'esdeveniments d'interfície d'usuari que per signatura no poden retornar cap altra cosa (es veu al Mòdul 7).
  • Oblidar l'await: escriure biblioteca.PrestarLlibreAsync(llibre1, soci1); sense await compila (amb un avís del compilador), però el mètode continua executant-se sense esperar que l'operació acabi; el préstec podria no estar registrat encara quan el codi següent l'assumeixi completat. Si el resultat d'un Task importa per al que segueix, sempre necessita el seu await.
  • Bloquejar amb .Result o .Wait() en lloc d'await: biblioteca.PrestarLlibreAsync(...).Wait(); "funciona", però bloqueja el fil actual exactament igual que el codi síncron de l'apartat 1 —anul·lant tot l'avantatge d'haver fet servir async/await, i podent provocar a més un interbloqueig (deadlock) en certs entorns (per exemple, aplicacions d'escriptori clàssiques amb un fil d'interfície dedicat). Fes servir sempre await en lloc de .Result/.Wait().
  • Creure que async fa que el codi "s'executi en un altre fil": async/await no crea fils nous per si mateix; el seu objectiu és alliberar el fil actual mentre s'espera una operació d'E/S, no repartir treball de càlcul entre diversos nuclis. Aquest altre problema —paral·lelisme de càlcul amb diversos fils— és el que resol el Mòdul 6 amb Thread i Parallel.
  • Consell: anomena sempre els mètodes asíncrons amb el sufix Async (PrestarLlibreAsync, no PrestarLlibre); és una convenció que tota la comunitat de C# segueix, i ajuda a distingir d'una ullada quins mètodes necessiten await al punt de crida.

Exercicis

  1. Escriu un mètode async Task<string> ObtenirTitolAleatoriAsync(Biblioteca biblioteca) que simuli una consulta lenta amb await Task.Delay(800) i després retorni el Titol del primer element de biblioteca.Cataleg (fes servir FirstOrDefault, recordant la lliçó de LINQ, i retorna un text alternatiu si el catàleg és buit). Crida'l des d'un Main asíncron amb await.

  2. Afegeix a Biblioteca un mètode async Task<bool> VerificarSancionsAsync(Soci soci) que simuli amb await Task.Delay(500) una consulta a un servei extern, i sempre retorni false (cap soci té sancions, de moment). Modifica PrestarLlibreAsync perquè cridi primer VerificarSancionsAsync(soci) i, si retorna true, llanci una InvalidOperationException abans de comprovar la disponibilitat del llibre.

  3. Fent servir Task.WhenAll, escriu un fragment de codi que presti, alhora, tres llibres diferents a tres socis diferents (pots reutilitzar PrestarLlibreAsync), i que mostri per consola un missatge en acabar només quan els tres préstecs s'hagin completat.

Solucions

async Task<string> ObtenirTitolAleatoriAsync(Biblioteca biblioteca)
{
    await Task.Delay(800);
    MaterialBibliotecari? primer = biblioteca.Cataleg.FirstOrDefault();
    return primer?.Titol ?? "El cataleg esta buit";
}

// Des d'un Main asincron:
string titol = await ObtenirTitolAleatoriAsync(biblioteca);
Console.WriteLine(titol);
public async Task<bool> VerificarSancionsAsync(Soci soci)
{
    await Task.Delay(500);
    return false; // simulacio: cap soci te sancions encara
}

public async Task PrestarLlibreAsync(Llibre llibre, Soci soci)
{
    bool teSancions = await VerificarSancionsAsync(soci);
    if (teSancions)
    {
        throw new InvalidOperationException($"{soci.Nom} te sancions pendents.");
    }

    Console.WriteLine($"Comprovant disponibilitat de '{llibre.Titol}'...");
    await Task.Delay(1000);

    if (!llibre.Disponible)
    {
        throw new InvalidOperationException($"'{llibre.Titol}' no esta disponible per a prestec.");
    }

    llibre.Prestar();
    Prestec prestec = new Prestec(llibre, soci);
    RegistrarPrestec(prestec);
}
Task prestec1 = biblioteca.PrestarLlibreAsync(llibre1, soci1);
Task prestec2 = biblioteca.PrestarLlibreAsync(llibre2, soci2);
Task prestec3 = biblioteca.PrestarLlibreAsync(llibre3, soci3);

await Task.WhenAll(prestec1, prestec2, prestec3);
Console.WriteLine("Els tres prestecs s'han completat.");

Cal notar que les tres crides a PrestarLlibreAsync es fan sense await en un primer moment (perquè les tres tasques arrenquin alhora), i només s'espera la seva finalització conjunta amb Task.WhenAll; si s'hagués escrit await a cada línia per separat, cada préstec hauria esperat que l'anterior acabés del tot abans de començar.

Conclusió

Amb async, await, Task/Task<T>, el tractament d'excepcions en codi asíncron i Task.WhenAll, es tanca el Mòdul 4 (Conceptes Avançats de C#). Repassant tot el recorregut: interfícies (IPrestable) per definir capacitats sense importar la jerarquia d'herència; delegats i esdeveniments (PrestecRegistrat) per notificar canvis d'estat; pattern matching i característiques modernes del llenguatge; genèrics (Repositori<T>) per escriure codi reutilitzable amb seguretat de tipus; col·leccions (List<T>, Dictionary<TKey, TValue>) que van completar finalment la classe Biblioteca; LINQ per consultar-la de forma expressiva; i, en aquesta última lliçó, PrestarLlibreAsync com a primera operació asíncrona del model. Biblioteca ha passat de ser una idea amb dues classes soltes a un model de domini complet, consultable i —ara— preparat per operar sense bloquejar un fil mentre espera.

Tot el que s'ha construït fins aquí, això sí, continua vivint únicament en memòria: si el programa acaba, tot el catàleg, els socis i l'historial de préstecs desapareixen amb ell. Aquesta és precisament la limitació que ataca el Mòdul 5 (Treballant amb Dades): la propera parada és substituir la llista en memòria de Biblioteca per persistència real, començant per Entrada/ Sortida de Fitxers i Serialització, seguint per Connectivitat amb Bases de Dades i Entity Framework, i acabant amb JSON i APIs REST. El Task.Delay simulat d'aquesta lliçó deixarà llavors de ser una simulació: PrestarLlibreAsync (i els mètodes que s'hi afegeixin) començaran a fer await sobre operacions d'E/S reals —llegir i escriure fitxers, consultar una base de dades— amb exactament el mateix async/await après aquí.

Curs de Programació en C#

Mòdul 1: Introducció al C#

Mòdul 2: Estructures de Control

Mòdul 3: Programació Orientada a Objectes

Mòdul 4: Conceptes Avançats de C#

Mòdul 5: Treballant amb Dades

Mòdul 6: Temes Avançats

Mòdul 7: Construcció d'Aplicacions

Mòdul 8: Bones Pràctiques i Patrons de Disseny

Mòdul 9: Projecte Final

© Copyright 2026. Tots els drets reservats