Todo el código de BiblioTech escrito hasta ahora es síncrono: cada línea espera a que la
anterior termine antes de ejecutarse, y si una operación tarda (una llamada de red, una lectura
de disco, una consulta a una base de datos), el hilo que la ejecuta se queda bloqueado, sin
poder hacer nada más, hasta que termina. En una aplicación real —una API web que atiende a
muchos socios de BiblioTech a la vez, o una aplicación de escritorio que no debe "congelarse"
mientras carga datos— este bloqueo tiene un coste real. La programación asíncrona, con las
palabras clave async y await, permite que un método ceda el control mientras espera una
operación lenta, sin bloquear el hilo, y lo recupere automáticamente cuando esa operación
termina. Esta lección introduce Task, async/await y el manejo de excepciones en código
asíncrono, y añade a Biblioteca un primer método PrestarLibroAsync que simula una operación
de E/S lenta, preparando el terreno para el Módulo 5, donde esa simulación se convertirá en E/S
real.
Contenido
- Código síncrono frente a código asíncrono: por qué importa
TaskyTask<T>: representando una operación en cursoasyncyawait: cómo se escribe un método asíncrono- Simulando una operación lenta con
Task.Delay - Manejo de excepciones en métodos
async - Varias tareas en paralelo con
Task.WhenAll PrestarLibroAsyncenBibliotecay una llamada desdeMainasíncrono
- Código síncrono frente a código asíncrono: por qué importa
Imagina que Biblioteca tuviera que consultar, antes de completar un préstamo, un servicio
externo que verifica si el socio tiene sanciones pendientes. Esa consulta —por red, o contra
una base de datos— puede tardar un segundo o más. Con código síncrono, el hilo que atiende
esa petición se queda completamente parado durante ese segundo:
// Sincrono: el hilo se bloquea por completo durante la espera
void PrestarLibro(Libro libro, Socio socio)
{
VerificarSancionesPendientes(socio); // tarda 1 segundo; el hilo no puede hacer nada mas mientras tanto
libro.Prestar();
}En una aplicación de consola pequeña, ese segundo de bloqueo apenas se nota. Pero en un servidor web que atiende a cientos de socios simultáneamente, cada hilo bloqueado esperando una verificación es un hilo que no puede atender a ningún otro socio mientras tanto: el servidor necesitaría cientos de hilos solo para estar "esperando", la mayoría de ellos sin hacer trabajo útil.
Con código asíncrono, el hilo se libera durante la espera y puede atender otras tareas; en cuanto la operación lenta termina, la ejecución del método continúa automáticamente, sin que el programador tenga que gestionar ese "reengancharse" manualmente:
| Código síncrono | Código asíncrono | |
|---|---|---|
| Durante una operación lenta (E/S) | El hilo se bloquea, esperando sin hacer nada más | El hilo queda libre para otras tareas |
| Palabra clave | Ninguna especial | async en la declaración, await en la llamada |
| Tipo de retorno habitual | void, T |
Task, Task<T> |
| Cuándo aporta valor | Cálculo puro en memoria (poco o nada de espera) | Operaciones de E/S: red, disco, base de datos |
Es importante no confundir asíncrono con "más rápido": una operación asíncrona no completa antes
por el hecho de serlo (Task.Delay(1000) sigue tardando un segundo). Lo que cambia es que,
mientras se espera, el hilo queda disponible para hacer otro trabajo en vez de quedar
bloqueado sin ninguna utilidad.
Task y Task<T>: representando una operación en curso
Task y Task<T>: representando una operación en cursoUn método asíncrono no devuelve directamente su resultado, sino un objeto Task (si no
devuelve ningún valor útil, el equivalente asíncrono de void) o Task<T> (si en algún momento
producirá un valor de tipo T). Un Task representa una operación que puede no haber
terminado todavía: es una especie de "recibo" que se puede consultar más adelante para
obtener el resultado, una vez esté disponible.
| Tipo | Equivalente síncrono | Cuándo se usa |
|---|---|---|
Task |
void |
El método asíncrono no produce ningún valor de retorno |
Task<T> |
T |
El método asíncrono producirá, eventualmente, un valor de tipo T |
Task<int> tareaConResultado = ObtenerNumeroDeSociosAsync(); // la tarea puede seguir en curso aqui
int numeroSocios = await tareaConResultado; // "await" espera a que termine y desenvuelve el resultadoawait tarea hace dos cosas a la vez: espera (sin bloquear el hilo) a que la Task termine, y
"desenvuelve" su resultado —de Task<int> se obtiene directamente un int, no un Task<int>
envuelto. Sobre una Task (sin resultado), await simplemente espera a que termine, sin
producir ningún valor.
async y await: cómo se escribe un método asíncrono
async y await: cómo se escribe un método asíncronoUn método se declara asíncrono añadiendo el modificador async a su firma, y devolviendo
Task o Task<T> en lugar de void o T. Dentro de su cuerpo, await se usa para esperar el
resultado de otra operación asíncrona:
async Task<string> ObtenerSaludoAsync()
{
await Task.Delay(500); // simula algo de trabajo asincrono (mas detalle en el siguiente apartado)
return "Hola desde un metodo asincrono";
}Tres reglas sintácticas que conviene fijar desde el principio:
- El modificador
asyncva antes del tipo de retorno, igual quepublicostatic. - El tipo de retorno declarado es
TaskoTask<T>; dentro del cuerpo, unreturn valor;se escribe igual que en un método síncrono que devolvieraTdirectamente —el compilador se encarga de envolverlo en elTask<T>. - Por convención, todo método asíncrono termina su nombre con el sufijo
Async(ObtenerSaludoAsync, y más adelantePrestarLibroAsync), para que quien lo llama sepa, solo por el nombre, que debe usarse conawait.
Para llamar a un método async, se usa await delante de la llamada, y el método que llama
debe, a su vez, ser también async:
async Task MostrarSaludoAsync()
{
string saludo = await ObtenerSaludoAsync(); // espera el resultado sin bloquear el hilo
Console.WriteLine(saludo);
}Esta propagación de async/await hacia arriba —cualquier método que use await debe ser
async, y quien lo llama probablemente también— se conoce como "async todo el camino"
(async all the way), y es la razón por la que Main necesita convertirse en asíncrono para
poder llamar a PrestarLibroAsync (apartado 7).
- Simulando una operación lenta con
Task.Delay
Task.DelayTask.Delay(milisegundos) devuelve una Task que termina, sin hacer ningún trabajo real, tras
el número de milisegundos indicado. Es la herramienta habitual para simular una operación
lenta (una llamada de red, una consulta a una base de datos) en ejemplos y pruebas, sin depender
todavía de ningún sistema externo real:
async Task SimularOperacionLentaAsync()
{
Console.WriteLine("Empieza la operacion...");
await Task.Delay(2000); // simula 2 segundos de espera, como una llamada de red real
Console.WriteLine("La operacion ha terminado.");
}Este es exactamente el papel que Task.Delay cumplirá en PrestarLibroAsync (apartado 7): en
esta lección todavía no hay ningún fichero, base de datos ni servicio real que consultar —eso
llega en el Módulo 5—, pero Task.Delay permite escribir y probar ya el flujo asíncrono
completo, con el mismo async/await que se usará después con E/S real.
- Manejo de excepciones en métodos
async
asyncUna de las ventajas de async/await frente a alternativas más antiguas de programación
asíncrona es que las excepciones se manejan exactamente igual que en código síncrono: un
try/catch normal alrededor de un await captura cualquier excepción que lance la operación
esperada, como si la llamada fuera síncrona:
async Task RealizarOperacionConRiesgoAsync()
{
await Task.Delay(500);
throw new InvalidOperationException("Algo ha ido mal durante la operacion.");
}
async Task ManejarOperacionAsync()
{
try
{
await RealizarOperacionConRiesgoAsync();
Console.WriteLine("Operacion completada sin errores.");
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"Error controlado: {ex.Message}");
}
}El try/catch envuelve el await, no la llamada al método en sí: la excepción se propaga de
forma natural desde dentro de la Task hasta el punto donde se espera su resultado, tal como
ocurriría con una excepción lanzada por una llamada síncrona directa. No hace falta ninguna
sintaxis especial adicional para "excepciones asíncronas": son excepciones normales,
manejadas con las herramientas que ya conoces de la lección de Manejo de Excepciones (Módulo
2).
- Varias tareas en paralelo con
Task.WhenAll
Task.WhenAllCuando hay varias operaciones asíncronas independientes entre sí (no necesita el resultado de
una para empezar la otra), esperarlas una detrás de otra con await desperdicia la ventaja de
la asincronía: si cada una tarda un segundo, esperarlas en secuencia tarda dos segundos en
total, aunque ninguna dependa de la otra.
// Secuencial: primero espera la primera tarea entera, luego la segunda -> unos 2 segundos en total
await SimularOperacionLentaAsync();
await SimularOperacionLentaAsync();
// En paralelo: las dos tareas se inician a la vez y se espera a que ambas terminen -> unos 1 segundo en total
Task tarea1 = SimularOperacionLentaAsync();
Task tarea2 = SimularOperacionLentaAsync();
await Task.WhenAll(tarea1, tarea2);Task.WhenAll recibe una o varias Task ya iniciadas (nótese que se llama al método sin
await para obtener el Task sin esperarlo todavía) y devuelve una única Task que termina
cuando todas han terminado. Es la herramienta habitual cuando se necesita lanzar varias
operaciones independientes —por ejemplo, varios préstamos a la vez— y esperar a que todas se
completen antes de continuar. Esta lección solo presenta Task.WhenAll de forma introductoria;
el Módulo 6 (Multihilo y Programación Paralela) profundiza en la ejecución concurrente real con
Thread y Parallel, un escenario distinto del que resuelve async/await (pensado, sobre
todo, para no bloquear un hilo mientras se espera E/S, no para repartir cálculo entre varios
núcleos de CPU).
PrestarLibroAsync en Biblioteca y una llamada desde Main asíncrono
PrestarLibroAsync en Biblioteca y una llamada desde Main asíncronoCon todo lo anterior, se añade a Biblioteca una versión asíncrona de un préstamo, que simula
—con Task.Delay— una comprobación lenta (por ejemplo, contra un futuro servicio de
sanciones) antes de completar el préstamo, reutilizando la lógica ya existente de Prestar() y
RegistrarPrestamo():
class Biblioteca
{
public List<MaterialBibliotecario> Catalogo { get; } = new List<MaterialBibliotecario>();
public List<Socio> Socios { get; } = new List<Socio>();
public List<Prestamo> Prestamos { get; } = new List<Prestamo>();
private Dictionary<int, Socio> _sociosPorId = new Dictionary<int, Socio>();
public event Action<Prestamo> PrestamoRegistrado;
// ... AgregarMaterial, AgregarSocio, BuscarSocioPorId, RegistrarPrestamo sin cambios ...
public async Task PrestarLibroAsync(Libro libro, Socio socio)
{
Console.WriteLine($"Comprobando disponibilidad de '{libro.Titulo}'...");
await Task.Delay(1000); // simula una verificacion lenta; en el Modulo 5 sera E/S real
if (!libro.Disponible)
{
throw new InvalidOperationException($"'{libro.Titulo}' no esta disponible para prestamo.");
}
libro.Prestar();
Prestamo prestamo = new Prestamo(libro, socio);
RegistrarPrestamo(prestamo);
}
}Ningún miembro anterior de Biblioteca cambia: PrestarLibroAsync es un método nuevo que
reutiliza libro.Prestar() (que actualiza Disponible y ya sabe informar por consola) y
RegistrarPrestamo(prestamo) (que añade el préstamo a Prestamos y dispara
PrestamoRegistrado), envolviendo esa lógica ya conocida en una espera asíncrona simulada. La
comprobación if (!libro.Disponible) se hace después del await Task.Delay(...), a
propósito: representa que la disponibilidad se re-verifica justo antes de confirmar el
préstamo, tras la "consulta lenta" simulada, y no antes.
Para llamar a PrestarLibroAsync hace falta un punto de entrada asíncrono. Desde C# 7.1,
Main puede declararse static async Task Main() (o static async Task Main(string[] args)):
class Program
{
static async Task Main()
{
Biblioteca biblioteca = new Biblioteca();
biblioteca.PrestamoRegistrado += prestamo =>
Console.WriteLine($"Se ha prestado '{prestamo.Libro.Titulo}' a {prestamo.Socio.Nombre}");
Libro libro1 = new Libro("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
Socio socio1 = new Socio(1, "Ana Martinez");
biblioteca.AgregarMaterial(libro1);
biblioteca.AgregarSocio(socio1);
try
{
await biblioteca.PrestarLibroAsync(libro1, socio1);
Console.WriteLine("Prestamo completado correctamente.");
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"No se pudo completar el prestamo: {ex.Message}");
}
}
}Si el proyecto usa top-level statements (el estilo de fichero único visto desde la lección
de Hello World, Módulo 1), no hace falta declarar Main explícitamente: basta con escribir
await directamente en el fichero, y el compilador genera un Main asíncrono por detrás:
// Program.cs, con top-level statements
Biblioteca biblioteca = new Biblioteca();
Libro libro1 = new Libro("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
Socio socio1 = new Socio(1, "Ana Martinez");
biblioteca.AgregarMaterial(libro1);
biblioteca.AgregarSocio(socio1);
await biblioteca.PrestarLibroAsync(libro1, socio1);
Console.WriteLine("Prestamo completado correctamente.");Y, retomando Task.WhenAll del apartado anterior, así se prestarían dos libros a la vez en
lugar de uno detrás de otro:
Libro libro2 = new Libro("Ficciones", "Jorge Luis Borges", "978-84-376-0496-1");
Socio socio2 = new Socio(2, "Luis Gomez");
biblioteca.AgregarMaterial(libro2);
biblioteca.AgregarSocio(socio2);
Task prestamo1 = biblioteca.PrestarLibroAsync(libro1, socio1);
Task prestamo2 = biblioteca.PrestarLibroAsync(libro2, socio2);
await Task.WhenAll(prestamo1, prestamo2); // ambas verificaciones de 1 segundo transcurren a la vezsequenceDiagram
participant Main as Main (async)
participant Bib as Biblioteca
participant Libro as Libro
Main->>Bib: await PrestarLibroAsync(libro1, socio1)
Bib->>Bib: await Task.Delay(1000)
Note over Bib: el hilo queda libre durante la espera
Bib->>Libro: Disponible?
Libro-->>Bib: true
Bib->>Libro: Prestar()
Bib->>Bib: RegistrarPrestamo(prestamo)
Bib-->>Main: Task completada
Errores Comunes y Consejos
async voiden cualquier sitio salvo manejadores de eventos: un métodoasync voidno devuelve ningúnTask, así que quien lo llama no puede usarawaitsobre él ni capturar sus excepciones con untry/catchnormal —una excepción no controlada dentro de unasync voidpuede llegar a tumbar el proceso. Usa siempreasync Task(oasync Task<T>);async voidse reserva, exclusivamente, para manejadores de eventos de UI que por firma no pueden devolver otra cosa (se ve en el Módulo 7).- Olvidar el
await: escribirbiblioteca.PrestarLibroAsync(libro1, socio1);sinawaitcompila (con un aviso del compilador), pero el método continúa ejecutándose sin esperar a que la operación termine; el préstamo podría no estar registrado todavía cuando el código siguiente lo asuma completado. Si el resultado de unTaskimporta para lo que sigue, siempre necesita suawait. - Bloquear con
.Resulto.Wait()en vez deawait:biblioteca.PrestarLibroAsync(...).Wait();"funciona", pero bloquea el hilo actual exactamente igual que el código síncrono del apartado 1 —anulando toda la ventaja de haber usadoasync/await, y pudiendo además provocar un interbloqueo (deadlock) en ciertos entornos (por ejemplo, aplicaciones de escritorio clásicas con un hilo de interfaz dedicado). Usa siempreawaiten vez de.Result/.Wait(). - Creer que
asynchace que el código "se ejecute en otro hilo":async/awaitno crea hilos nuevos por sí mismo; su objetivo es liberar el hilo actual mientras se espera una operación de E/S, no repartir trabajo de cálculo entre varios núcleos. Ese otro problema —paralelismo de cálculo con varios hilos— es el que resuelve el Módulo 6 conThreadyParallel. - Consejo: nombra siempre los métodos asíncronos con el sufijo
Async(PrestarLibroAsync, noPrestarLibro); es una convención que toda la comunidad de C# sigue, y ayuda a distinguir de un vistazo qué métodos necesitanawaiten el punto de llamada.
Ejercicios
-
Escribe un método
async Task<string> ObtenerTituloAleatorioAsync(Biblioteca biblioteca)que simule una consulta lenta conawait Task.Delay(800)y después devuelva elTitulodel primer elemento debiblioteca.Catalogo(usaFirstOrDefault, recordando la lección de LINQ, y devuelve un texto alternativo si el catálogo está vacío). Llámalo desde unMainasíncrono conawait. -
Añade a
Bibliotecaun métodoasync Task<bool> VerificarSancionesAsync(Socio socio)que simule conawait Task.Delay(500)una consulta a un servicio externo, y siempre devuelvafalse(ningún socio tiene sanciones, de momento). ModificaPrestarLibroAsyncpara que llame primero aVerificarSancionesAsync(socio)y, si devuelvetrue, lance unaInvalidOperationExceptionantes de comprobar la disponibilidad del libro. -
Usando
Task.WhenAll, escribe un fragmento de código que preste, a la vez, tres libros distintos a tres socios distintos (puedes reutilizarPrestarLibroAsync), y que muestre por consola un mensaje al terminar solo cuando los tres préstamos se hayan completado.
Soluciones
async Task<string> ObtenerTituloAleatorioAsync(Biblioteca biblioteca)
{
await Task.Delay(800);
MaterialBibliotecario? primero = biblioteca.Catalogo.FirstOrDefault();
return primero?.Titulo ?? "El catalogo esta vacio";
}
// Desde un Main asincrono:
string titulo = await ObtenerTituloAleatorioAsync(biblioteca);
Console.WriteLine(titulo);
public async Task<bool> VerificarSancionesAsync(Socio socio)
{
await Task.Delay(500);
return false; // simulacion: ningun socio tiene sanciones todavia
}
public async Task PrestarLibroAsync(Libro libro, Socio socio)
{
bool tieneSanciones = await VerificarSancionesAsync(socio);
if (tieneSanciones)
{
throw new InvalidOperationException($"{socio.Nombre} tiene sanciones pendientes.");
}
Console.WriteLine($"Comprobando disponibilidad de '{libro.Titulo}'...");
await Task.Delay(1000);
if (!libro.Disponible)
{
throw new InvalidOperationException($"'{libro.Titulo}' no esta disponible para prestamo.");
}
libro.Prestar();
Prestamo prestamo = new Prestamo(libro, socio);
RegistrarPrestamo(prestamo);
}
Task prestamo1 = biblioteca.PrestarLibroAsync(libro1, socio1);
Task prestamo2 = biblioteca.PrestarLibroAsync(libro2, socio2);
Task prestamo3 = biblioteca.PrestarLibroAsync(libro3, socio3);
await Task.WhenAll(prestamo1, prestamo2, prestamo3);
Console.WriteLine("Los tres prestamos se han completado.");
Nótese que las tres llamadas a PrestarLibroAsync se hacen sin await en un primer
momento (para que las tres tareas arranquen a la vez), y solo se espera su finalización
conjunta con Task.WhenAll; si se hubiera escrito await en cada línea por separado, cada
préstamo habría esperado a que el anterior terminase del todo antes de empezar.
Conclusión
Con async, await, Task/Task<T>, el manejo de excepciones en código asíncrono y
Task.WhenAll, se cierra el Módulo 4 (Conceptos Avanzados de C#). Repasando todo lo recorrido:
interfaces (IPrestable) para definir capacidades sin importar la jerarquía de herencia;
delegados y eventos (PrestamoRegistrado) para notificar cambios de estado; pattern matching y
características modernas del lenguaje; genéricos (Repositorio<T>) para escribir código
reutilizable con seguridad de tipos; colecciones (List<T>, Dictionary<TKey, TValue>) que
completaron por fin la clase Biblioteca; LINQ para consultarla de forma expresiva; y, en esta
última lección, PrestarLibroAsync como primera operación asíncrona del modelo. Biblioteca ha
pasado de ser una idea con dos clases sueltas a un modelo de dominio completo, consultable y
—ahora— preparado para operar sin bloquear un hilo mientras espera.
Todo lo construido hasta aquí, eso sí, sigue viviendo únicamente en memoria: si el programa
termina, todo el catálogo, los socios y el historial de préstamos desaparecen con él. Esa es
precisamente la limitación que ataca el Módulo 5 (Trabajando con Datos): la próxima parada es
sustituir la lista en memoria de Biblioteca por persistencia real, empezando por
Entrada/Salida de Archivos y Serialización, siguiendo por Conectividad con Bases de Datos y
Entity Framework, y terminando con JSON y APIs REST. El Task.Delay simulado de esta lección
dejará entonces de ser una simulación: PrestarLibroAsync (y los métodos que se añadan junto a
él) empezarán a hacer await sobre operaciones de E/S reales —leer y escribir ficheros,
consultar una base de datos— con exactamente el mismo async/await aprendido aquí.
Curso de Programación en C#
Módulo 1: Introducción a C#
- Introducción a C#
- Configuración del Entorno de Desarrollo
- Programa Hola Mundo
- Sintaxis y Estructura Básica
- Variables y Tipos de Datos
- Arrays y Cadenas de Texto
Módulo 2: Estructuras de Control
Módulo 3: Programación Orientada a Objetos
- Clases y Objetos
- Métodos
- Constructores y Destructores
- Herencia
- Polimorfismo
- Encapsulamiento
- Abstracción
- Structs y Records: Tipos por Valor y por Referencia
Módulo 4: Conceptos Avanzados de C#
- Interfaces
- Delegados y Eventos
- Pattern Matching y Características Modernas de C#
- Genéricos
- Colecciones
- LINQ (Consulta Integrada en el Lenguaje)
- Programación Asíncrona
Módulo 5: Trabajando con Datos
- Entrada/Salida de Archivos
- Serialización
- Conectividad con Bases de Datos
- Entity Framework
- Trabajo con JSON y Consumo de APIs REST
Módulo 6: Temas Avanzados
- Reflexión
- Atributos
- Programación Dinámica
- Gestión de Memoria y Recolección de Basura
- Multihilo y Programación Paralela
Módulo 7: Construcción de Aplicaciones
- Formularios de Windows
- WPF (Windows Presentation Foundation)
- ASP.NET Core
- Blazor
- Xamarin y .NET MAUI
Módulo 8: Mejores Prácticas y Patrones de Diseño
- Estándares de Codificación y Mejores Prácticas
- Patrones de Diseño
- Inyección de Dependencias e Inversión de Control
- Pruebas Unitarias
- Revisión y Refactorización de Código
