Cargando...

Fundamentos de programación asíncrona en C# (async/await)

Aprende async y await en C# para crear aplicaciones fluidas con tareas asíncronas y ejemplos prácticos.

En las aplicaciones modernas, la programación asíncrona es fundamental para mantener la interfaz de usuario fluida y permitir que las operaciones de larga duración se ejecuten sin congelar la aplicación. En C#, se utilizan las palabras clave async y await para este propósito. Esta estructura permite ejecutar tareas largas en segundo plano mientras la aplicación sigue respondiendo.


¿Qué es la programación asíncrona?

La programación asíncrona permite que otras operaciones continúen mientras se espera que una tarea termine. Es especialmente útil para operaciones I/O-bound como la lectura de archivos, solicitudes de red, consultas a bases de datos o llamadas a APIs.


// Sincrónico: las operaciones se ejecutan secuencialmente
LeerArchivo();
EnviarDatos();
Console.WriteLine("¡Completado!");

// Asíncrono: las operaciones continúan sin esperar
await LeerArchivoAsync();
await EnviarDatosAsync();
Console.WriteLine("¡Completado!");

Las palabras clave async y await

La palabra clave async marca un método como asíncrono. Dentro de este método, se usa await para esperar que otras tareas asíncronas terminen. Esto permite que el hilo continúe ejecutando otras operaciones sin bloquearse.


async Task EjecutarOperacionAsync()
{
    Console.WriteLine("Operación iniciada...");
    await Task.Delay(2000); // Espera 2 segundos, pero no bloquea el hilo
    Console.WriteLine("¡Operación completada!");
}

En el ejemplo anterior, aunque Task.Delay introduce una espera, la aplicación no se congela, ya que la tarea espera en segundo plano sin bloquear la CPU.


Tipos Task y Task<T>

Los métodos asíncronos normalmente devuelven un Task o un Task<T>. Task solo indica que la operación ha finalizado, mientras que Task<T> devuelve un valor.


// Método asíncrono sin valor de retorno
async Task GuardarArchivoAsync(string nombre)
{
    await File.WriteAllTextAsync(nombre, "Contenido del archivo...");
}

// Método asíncrono con valor de retorno
async Task<string> ObtenerDatosAsync()
{
    await Task.Delay(1000);
    return "Datos recibidos del servidor";
}

// Uso:
string resultado = await ObtenerDatosAsync();
Console.WriteLine(resultado);

Método Main asíncrono

Desde C# 7.1, el método Main también puede ser asíncrono. Esto permite usar await directamente en aplicaciones de consola.


class Program
{
    static async Task Main()
    {
        Console.WriteLine("Obteniendo datos...");
        string datos = await ObtenerDatosAsync();
        Console.WriteLine($"Resultado: {datos}");
    }

    static async Task<string> ObtenerDatosAsync()
    {
        await Task.Delay(1500);
        return "¡Completado!";
    }
}

Aspectos importantes al usar await


// Ejecutar varias tareas en paralelo
var tarea1 = LeerArchivoAsync();
var tarea2 = EnviarDatosAsync();
await Task.WhenAll(tarea1, tarea2);
Console.WriteLine("Ambas tareas se completaron.");

Manejo de errores en código asíncrono

En los métodos asíncronos, los errores se manejan con bloques try-catch. Si una tarea esperada mediante await genera una excepción, se ejecutará el bloque catch.


try
{
    await LeerArchivoAsync();
}
catch (IOException ex)
{
    Console.WriteLine($"Error al leer el archivo: {ex.Message}");
}

Ejemplo: Obtener datos desde una API

En el siguiente ejemplo se realiza una solicitud HTTP asíncrona utilizando HttpClient. La palabra clave await evita que la aplicación se bloquee mientras espera la respuesta de la red.


using System;
using System.Net.Http;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        using HttpClient client = new HttpClient();
        string url = "https://jsonplaceholder.typicode.com/posts/1";
        Console.WriteLine("Solicitando datos...");

        string json = await client.GetStringAsync(url);
        Console.WriteLine("JSON recibido:");
        Console.WriteLine(json);
    }
}

Ejemplo WPF: Operación asíncrona y actualización de la interfaz

En el siguiente ejemplo, cuando se hace clic en un botón dentro de una aplicación WPF, se ejecuta una operación de larga duración (simulada con Task.Delay) de forma asíncrona. Gracias a la palabra clave await, la interfaz de usuario permanece receptiva durante la operación, y el contenido del TextBox puede actualizarse sin que la aplicación se congele.


// MainWindow.xaml
<Window x:Class="AsyncExample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Ejemplo Async / Await" Height="250" Width="400">
    <Grid Margin="20">
        <StackPanel>
            <TextBlock Text="Ejemplo de operación asíncrona" FontWeight="Bold" FontSize="16" Margin="0,0,0,10"/>
            <TextBox x:Name="txtEstado" Height="30" Margin="0,0,0,10" 
                     VerticalContentAlignment="Center" FontSize="14"/>
            <Button x:Name="btnIniciar" Height="35" Content="Iniciar operación" 
                    Click="btnIniciar_Click" FontSize="14"/>
        </StackPanel>
    </Grid>
</Window>

// MainWindow.xaml.cs
using System;
using System.Threading.Tasks;
using System.Windows;

namespace AsyncExample
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private async void btnIniciar_Click(object sender, RoutedEventArgs e)
        {
            // async void es válido aquí porque es un manejador de eventos
            txtEstado.Text = "Operación iniciada...";
            btnIniciar.IsEnabled = false; // desactivar el botón

            // Simulación de una operación larga (por ejemplo: descarga de archivo)
            await Task.Delay(3000);

            txtEstado.Text = "¡Operación completada!";
            btnIniciar.IsEnabled = true;
        }
    }
}

// Flujo de ejecución:
1. El usuario hace clic en el botón "Iniciar operación".
2. El TextBox se actualiza con "Operación iniciada...".
3. Se espera 3 segundos (la interfaz sigue siendo receptiva).
4. Cuando termina, el TextBox cambia a "¡Operación completada!".

Nota: Si este código se ejecutara de forma sincrónica (por ejemplo, usando Thread.Sleep()), la interfaz de usuario se congelaría y el texto "Operación iniciada..." no aparecería hasta que todo termine. El uso de await Task.Delay() elimina completamente este problema.


Ejemplo WPF: Actualizar TextBox desde una tarea (usando Dispatcher)

En este ejemplo, una operación de larga duración se ejecuta dentro de una tarea separada (Task). Las actualizaciones de txtEstado.Text se realizan en el hilo de la interfaz (UI Thread) utilizando Dispatcher.Invoke. Esto evita errores de acceso entre varios hilos.


// MainWindow.xaml
<Window x:Class="AsyncExample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Ejemplo Dispatcher" Height="250" Width="400">
    <Grid Margin="20">
        <StackPanel>
            <TextBlock Text="Actualizar UI dentro de una tarea" FontWeight="Bold" FontSize="16" Margin="0,0,0,10"/>
            <TextBox x:Name="txtEstado" Height="30" Margin="0,0,0,10"
                     VerticalContentAlignment="Center" FontSize="14"/>
            <Button x:Name="btnIniciar" Height="35" Content="Iniciar operación larga"
                    Click="btnIniciar_Click" FontSize="14"/>
        </StackPanel>
    </Grid>
</Window>

// MainWindow.xaml.cs
using System;
using System.Threading.Tasks;
using System.Windows;

namespace AsyncExample
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private async void btnIniciar_Click(object sender, RoutedEventArgs e)
        {
            btnIniciar.IsEnabled = false;
            txtEstado.Text = "Proceso iniciado...";

            // Ejecutar operación larga dentro de una tarea
            await Task.Run(() =>
            {
                for (int i = 1; i <= 5; i++)
                {
                    // Simular trabajo
                    Task.Delay(1000).Wait();

                    // Actualización de la UI usando Dispatcher
                    Dispatcher.Invoke(() =>
                    {
                        txtEstado.Text = $"Paso {i} completado...";
                    });
                }
            });

            txtEstado.Text = "¡Todos los pasos completados!";
            btnIniciar.IsEnabled = true;
        }
    }
}

// Flujo de ejecución:
1. Se hace clic en el botón "Iniciar operación larga".
2. Se inicia un proceso de 5 pasos dentro de Task.Run.
3. Cada segundo, Dispatcher.Invoke actualiza el TextBox.
4. La interfaz sigue respondiendo en todo momento.

Nota: Si se reemplazara Dispatcher.Invoke() por una asignación directa como txtEstado.Text = ..., se produciría un error por acceso desde un hilo distinto al de la interfaz. Alternativamente, se puede usar Dispatcher.BeginInvoke(), el cual no bloquea el hilo de la interfaz.


TL;DR (Resumen)

  • async convierte un método en asíncrono, await espera a que termine la operación.
  • Task y Task<T> representan operaciones asíncronas y sus resultados.
  • await no bloquea la CPU; la ejecución continúa cuando la tarea finaliza.
  • Con Task.WhenAll() se pueden ejecutar varias tareas simultáneamente.
  • Los errores se manejan con try-catch; async void debe usarse solo en manejadores de eventos.

Artículos relacionados