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

  1. Código síncrono frente a código asíncrono: por qué importa
  2. Task y Task<T>: representando una operación en curso
  3. async y await: cómo se escribe un método asíncrono
  4. Simulando una operación lenta con Task.Delay
  5. Manejo de excepciones en métodos async
  6. Varias tareas en paralelo con Task.WhenAll
  7. PrestarLibroAsync en Biblioteca y una llamada desde Main asíncrono

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

  1. Task y Task<T>: representando una operación en curso

Un 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 resultado

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

  1. async y await: cómo se escribe un método asíncrono

Un 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 async va antes del tipo de retorno, igual que public o static.
  • El tipo de retorno declarado es Task o Task<T>; dentro del cuerpo, un return valor; se escribe igual que en un método síncrono que devolviera T directamente —el compilador se encarga de envolverlo en el Task<T>.
  • Por convención, todo método asíncrono termina su nombre con el sufijo Async (ObtenerSaludoAsync, y más adelante PrestarLibroAsync), para que quien lo llama sepa, solo por el nombre, que debe usarse con await.

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

  1. Simulando una operación lenta con Task.Delay

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

  1. Manejo de excepciones en métodos async

Una 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).

  1. Varias tareas en paralelo con Task.WhenAll

Cuando 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).

  1. PrestarLibroAsync en Biblioteca y una llamada desde Main asíncrono

Con 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 vez
sequenceDiagram
    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 void en cualquier sitio salvo manejadores de eventos: un método async void no devuelve ningún Task, así que quien lo llama no puede usar await sobre él ni capturar sus excepciones con un try/catch normal —una excepción no controlada dentro de un async void puede llegar a tumbar el proceso. Usa siempre async Task (o async Task<T>); async void se 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: escribir biblioteca.PrestarLibroAsync(libro1, socio1); sin await compila (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 un Task importa para lo que sigue, siempre necesita su await.
  • Bloquear con .Result o .Wait() en vez de await: 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 usado async/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 siempre await en vez de .Result/.Wait().
  • Creer que async hace que el código "se ejecute en otro hilo": async/await no 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 con Thread y Parallel.
  • Consejo: nombra siempre los métodos asíncronos con el sufijo Async (PrestarLibroAsync, no PrestarLibro); es una convención que toda la comunidad de C# sigue, y ayuda a distinguir de un vistazo qué métodos necesitan await en el punto de llamada.

Ejercicios

  1. Escribe un método async Task<string> ObtenerTituloAleatorioAsync(Biblioteca biblioteca) que simule una consulta lenta con await Task.Delay(800) y después devuelva el Titulo del primer elemento de biblioteca.Catalogo (usa FirstOrDefault, recordando la lección de LINQ, y devuelve un texto alternativo si el catálogo está vacío). Llámalo desde un Main asíncrono con await.

  2. Añade a Biblioteca un método async Task<bool> VerificarSancionesAsync(Socio socio) que simule con await Task.Delay(500) una consulta a un servicio externo, y siempre devuelva false (ningún socio tiene sanciones, de momento). Modifica PrestarLibroAsync para que llame primero a VerificarSancionesAsync(socio) y, si devuelve true, lance una InvalidOperationException antes de comprobar la disponibilidad del libro.

  3. Usando Task.WhenAll, escribe un fragmento de código que preste, a la vez, tres libros distintos a tres socios distintos (puedes reutilizar PrestarLibroAsync), 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#

Módulo 2: Estructuras de Control

Módulo 3: Programación Orientada a Objetos

Módulo 4: Conceptos Avanzados de C#

Módulo 5: Trabajando con Datos

Módulo 6: Temas Avanzados

Módulo 7: Construcción de Aplicaciones

Módulo 8: Mejores Prácticas y Patrones de Diseño

Módulo 9: Proyecto Final

© Copyright 2026. Todos los derechos reservados