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
- Codi síncron enfront de codi asíncron: per què importa
TaskiTask<T>: representant una operació en cursasynciawait: com s'escriu un mètode asíncron- Simulant una operació lenta amb
Task.Delay - Tractament d'excepcions en mètodes
async - Diverses tasques en paral·lel amb
Task.WhenAll PrestarLlibreAsyncaBibliotecai una crida des d'unMainasíncron
- 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.
Task i Task<T>: representant una operació en curs
Task i Task<T>: representant una operació en cursUn 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 resultatawait 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.
async i await: com s'escriu un mètode asíncron
async i await: com s'escriu un mètode asíncronUn 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
asyncva abans del tipus de retorn, igual quepublicostatic. - El tipus de retorn declarat és
TaskoTask<T>; dins del cos, unreturn valor;s'escriu igual que en un mètode síncron que retornésTdirectament —el compilador s'encarrega d'embolicar-lo alTask<T>. - Per convenció, tot mètode asíncron acaba el seu nom amb el sufix
Async(ObtenirSalutacioAsync, i més endavantPrestarLlibreAsync), perquè qui el crida sàpiga, només pel nom, que s'ha de fer servir ambawait.
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).
- Simulant una operació lenta amb
Task.Delay
Task.DelayTask.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.
- Tractament d'excepcions en mètodes
async
asyncUn 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).
- Diverses tasques en paral·lel amb
Task.WhenAll
Task.WhenAllQuan 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).
PrestarLlibreAsync a Biblioteca i una crida des d'un Main asíncron
PrestarLlibreAsync a Biblioteca i una crida des d'un Main asíncronAmb 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 alhorasequenceDiagram
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 voiden qualsevol lloc tret de gestors d'esdeveniments: un mètodeasync voidno retorna capTask, així que qui el crida no pot fer servirawaitsobre ell ni capturar les seves excepcions amb untry/catchnormal —una excepció no controlada dins d'unasync voidpot arribar a tombar el procés. Fes servir sempreasync Task(oasync Task<T>);async voides 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: escriurebiblioteca.PrestarLlibreAsync(llibre1, soci1);senseawaitcompila (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'unTaskimporta per al que segueix, sempre necessita el seuawait. - Bloquejar amb
.Resulto.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 servirasync/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 sempreawaiten lloc de.Result/.Wait(). - Creure que
asyncfa que el codi "s'executi en un altre fil":async/awaitno 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 ambThreadiParallel. - Consell: anomena sempre els mètodes asíncrons amb el sufix
Async(PrestarLlibreAsync, noPrestarLlibre); és una convenció que tota la comunitat de C# segueix, i ajuda a distingir d'una ullada quins mètodes necessitenawaital punt de crida.
Exercicis
-
Escriu un mètode
async Task<string> ObtenirTitolAleatoriAsync(Biblioteca biblioteca)que simuli una consulta lenta ambawait Task.Delay(800)i després retorni elTitoldel primer element debiblioteca.Cataleg(fes servirFirstOrDefault, recordant la lliçó de LINQ, i retorna un text alternatiu si el catàleg és buit). Crida'l des d'unMainasíncron ambawait. -
Afegeix a
Bibliotecaun mètodeasync Task<bool> VerificarSancionsAsync(Soci soci)que simuli ambawait Task.Delay(500)una consulta a un servei extern, i sempre retornifalse(cap soci té sancions, de moment). ModificaPrestarLlibreAsyncperquè cridi primerVerificarSancionsAsync(soci)i, si retornatrue, llanci unaInvalidOperationExceptionabans de comprovar la disponibilitat del llibre. -
Fent servir
Task.WhenAll, escriu un fragment de codi que presti, alhora, tres llibres diferents a tres socis diferents (pots reutilitzarPrestarLlibreAsync), 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#
- Introducció al C#
- Configuració de l'Entorn de Desenvolupament
- Programa Hola Món
- Sintaxi i Estructura Bàsica
- Variables i Tipus de Dades
- Arrays i Cadenes de Text
Mòdul 2: Estructures de Control
Mòdul 3: Programació Orientada a Objectes
- Classes i Objectes
- Mètodes
- Constructors i Destructors
- Herència
- Polimorfisme
- Encapsulació
- Abstracció
- Structs i Records: Tipus per Valor i per Referència
Mòdul 4: Conceptes Avançats de C#
- Interfícies
- Delegats i Esdeveniments
- Pattern Matching i Característiques Modernes de C#
- Genèrics
- Col·leccions
- LINQ (Consulta Integrada en el Llenguatge)
- Programació Asíncrona
Mòdul 5: Treballant amb Dades
- Entrada/Sortida de Fitxers
- Serialització
- Connectivitat amb Bases de Dades
- Entity Framework
- Treball amb JSON i Consum d'APIs REST
Mòdul 6: Temes Avançats
- Reflexió
- Atributs
- Programació Dinàmica
- Gestió de Memòria i Recollida d'Escombraries
- Multifil i Programació Paral·lela
Mòdul 7: Construcció d'Aplicacions
Mòdul 8: Bones Pràctiques i Patrons de Disseny
- Estàndards de Codificació i Bones Pràctiques
- Patrons de Disseny
- Injecció de Dependències i Inversió de Control
- Proves Unitàries
- Revisió i Refactorització de Codi
