CodeGym /Cursos /C# SELF /Clases Task y

Clases Task y Task<TResult>

C# SELF
Nivel 60 , Lección 0
Disponible

1. Introducción

En el mundo de la programación asíncrona vivimos según el principio de "delegar con retroalimentación". Imagínate: necesitas descargar un archivo enorme, analizar gigabytes de logs o hacer una petición a un servidor lejano. En vez de quedarte congelado esperando como una estatua, le decimos al sistema: "Ocúpate de esto, yo mientras hago otra cosa. Cuando termines —¡avísame!"

Ahí entra en escena Task — la encarnación elegante de ese enfoque. No es solo una abstracción técnica, es una especie de "intermediario inteligente" que se hace cargo de ejecutar el trabajo y garantiza que el resultado no se pierda en el vacío digital.

Task funciona como un asistente personal al que le encargas una tarea importante. Asiente, anota en su bloc y dice: "Anda tranquilo a lo tuyo, yo te aviso cuando esté listo". Y efectivamente te avisa — con el resultado en la mano o con una explicación honesta de por qué algo salió mal.

Si buscas una analogía más cotidiana, Task se parece a un sistema moderno de cita previa: te registras online para ver al médico, recibes confirmación y no necesitas pasar horas en la sala de espera. El sistema te recordará la cita, y tú mientras puedes seguir con tu vida.

La clase Task

Task es el bloque básico de construcción de la programación asíncrona en .NET. Representa una operación iniciada o futura cuyo resultado estará disponible más adelante. Si un método no debe devolver nada, usamos simplemente Task.

public async Task BackupToCloudAsync()
{
    // Hace la magia del backup, no devuelve nada
}

La clase Task<TResult>

Si necesitas devolver un resultado (por ejemplo, una cadena, un número, un objeto…), usamos Task<TResult>:

public async Task<string> DownloadHtmlAsync(string url)
{
    // Descarga la página y devuelve el HTML
    return "<html>...</html>";
}

¿Por qué Task y no Thread?

Thread gestiona el hilo en sí (eso es pesado y peligroso), mientras que Task es una abstracción de más alto nivel: puede ejecutarse en el thread pool, puede trabajar de forma asíncrona sin asignar un hilo nuevo (p. ej. en operaciones I/O) y no te obliga a ocuparte de detalles de bajo nivel.

La clase Task te permite describir: "Quiero ejecutar esta acción", y que .NET se encargue de decidir cómo ejecutarla.

2. Estructura del objeto Task

Propiedades y métodos de Task que debes conocer

Propiedad / Método Descripción
Status
Estado actual de la tarea
Result
Resultado para Task<TResult> (bloquea el hilo)
IsCompleted
Si la tarea está completada
IsFaulted
Si ocurrió una excepción en la tarea
IsCanceled
Si la tarea fue cancelada
Wait()
Bloquea el hilo actual hasta completarse (peligroso)
ContinueWith()
Ejecutar otra tarea después de completar
Exception
Acceso a la excepción si el Task terminó con error
Id
Identificador único de la tarea

Cómo funciona un método asíncrono con Task

sequenceDiagram
    participant Main as Hilo Main
    participant Task as Task (Tarea en background)
    Main->>Task: Inicio Task.Run(() => ...)
    Note right of Task: Ejecución en background
(CPU o I/O) alt Tarea completada Task->>Main: await finalizó, continuamos else Error Task->>Main: await lanza excepción end

3. Creación y ejecución de tareas: cómo funciona Task

Métodos async con async

El caso más común — declaras el método como async y devuelves o bien Task o Task<TResult> (como vimos antes).

Task.Run: ejecución en el thread pool

Si necesitas ejecutar un trabajo pesado en background (por ejemplo, calcular números grandes o codificar video), puedes usar Task.Run:

Task work = Task.Run(() =>
{
    // Cálculos pesados — no bloquees el hilo principal!
    Console.WriteLine("Cálculos en background iniciados...");
    Thread.Sleep(2000); // Emulación de trabajo largo
    Console.WriteLine("Cálculos en background terminados!");
});

Si es útil obtener un resultado:

Task<int> calculateTask = Task.Run(() =>
{
    // Por ejemplo, sumar los primeros 100 números
    int sum = 0;
    for (int i = 1; i <= 100; i++) sum += i;
    return sum;
});

Task.Factory.StartNew

Es una forma más de bajo nivel y flexible, permite ajustar finamente el inicio de la tarea (p. ej. especificar el scheduler, pasar parámetros, etc.). En código moderno casi siempre se recomienda usar Task.Run, porque es más simple y evita errores.

4. Aplicación del día: nuestro catálogo de libros

Supongamos que tenemos una aplicación de catálogo de libros y necesitamos añadir una función para cargar libros desde una fuente "en la nube" — esto será una operación I/O-bound (petición HTTP lenta o lectura de archivo).

Añadamos un método que "carga" libros de forma asíncrona (emulamos la latencia):

public class Book
{
    public string Title { get; set; }
    public string Author { get; set; }
}

public class BookCatalog
{
    public List<Book> Books { get; set; } = new();

    public async Task LoadBooksAsync()
    {
        Console.WriteLine("Cargando libros...");
        await Task.Delay(2000); // Emulación de carga larga (p. ej. HTTP o archivo)
        Books = new List<Book>
        {
            new Book { Title = "CLR via C#", Author = "Jeffrey Richter" },
            new Book { Title = "C# in Depth", Author = "Jon Skeet" }
        };
        Console.WriteLine("Libros cargados con éxito.");
    }
}

En Main llamamos a la carga asíncrona (usando await):

var catalog = new BookCatalog();
await catalog.LoadBooksAsync();
Console.WriteLine($"En el catálogo hay {catalog.Books.Count} libros.");

Tabla: Principales formas de crear y arrancar Task

Método de creación Cómo se aplica Resultado Uso
método async
async Task / async Task<T>
Operación asíncrona Normalmente I/O, comodidad
Task.Run
Task.Run(() => { ... })
Tarea en background CPU-bound (cálculos)
TaskCompletionSource<T>
Creas y completas el Task manualmente Control total para el programador Raro, para cosas de bajo nivel

5. Ciclo de vida de Task

Una Task puede estar en varios estados:

  • Created — la tarea está creada pero no iniciada (para Task con inicio explícito).
  • WaitingToRun — esperando en la cola del pool.
  • Running — se está ejecutando.
  • WaitingForActivation — esperando a ser activada o iniciada externamente.
  • RanToCompletion — finalizó con éxito.
  • Faulted — terminó con error (excepción).
  • Canceled — cancelada (si soporta cancelación).

Diagrama

flowchart LR
    Start -->|Inicio tarea| Running
    Running -->|Éxito| Completed
    Running -->|Error| Faulted
    Running -->|Cancelación| Canceled

Probémoslo en práctica

Task task = Task.Run(() =>
{
    Thread.Sleep(1000);
});
Console.WriteLine(task.Status); // Normalmente: Running o WaitingToRun
await task;
Console.WriteLine(task.Status); // RanToCompletion después de finalizar

6. ¿Cómo obtener el resultado de Task<TResult>?

Task<TResult> es un contenedor del resultado que aparecerá en el futuro. Cuando necesites esperar el resultado, usa await:

Task<int> sumTask = Task.Run(() =>
{
    int sum = 0;
    for (int i = 1; i <= 5; i++) sum += i;
    return sum;
});

int result = await sumTask;
Console.WriteLine(result); // 15

Si te olvidas de escribir await, obtendrás un Task (una promesa), no el resultado. Es una trampa típica de la asincronía.

Alternativa: obtener el resultado de forma síncrona (¡NO HACER EN UI!)

A veces (p. ej. en tests) necesitas el resultado sin await. Puedes usar la propiedad .Result:

int result = sumTask.Result;

Pero si el Task aún no ha terminado, este código bloquea el hilo, y si es el hilo de UI, la aplicación se congelará. Por eso: siempre que sea posible, prefiere await.

Errores típicos con Task y Task<TResult>

Olvidar devolver Task, el método pasó a ser void. Si el método no tiene valor de retorno — devuelve Task, no void, porque entonces no podrás manejar errores.

Ignorar await. Llamas al método sin esperar, y la tarea vive su propia vida ("fire and forget"). Ya no sabrás cuándo terminó o si falló.

Esperar bloqueando vía .Result o .Wait(). Fácil provocar deadlock, especialmente en UI y ASP.NET. Usa siempre await.

7. Capacidades avanzadas de Task

Encadenar tareas: ContinueWith

Puedes "enganchar" acciones que deben ejecutarse después de que una tarea termine, usando ContinueWith:

Task.Run(() => 10)
    .ContinueWith(t =>
    {
        Console.WriteLine($"¡Hecho! Resultado: {t.Result}");
    });

Pero en C# moderno normalmente se hace con async/await — así es más legible.

Ejemplo: carga paralela y secuencial de datos

Supón que tienes que cargar dos listas de libros desde fuentes distintas. Puedes arrancar ambos Task en paralelo y esperar a ambos:

public async Task LoadBooksFromMultipleSourcesAsync()
{
    Task<List<Book>> t1 = LoadFromCloudAsync();
    Task<List<Book>> t2 = LoadFromLocalAsync();

    // Esperamos ambas tareas en paralelo
    await Task.WhenAll(t1, t2);

    // Combinamos resultados
    Books = t1.Result.Concat(t2.Result).ToList();
}

private async Task<List<Book>> LoadFromCloudAsync()
{
    await Task.Delay(2000); // "Nube"
    return new List<Book> { new Book { Title = "Cloud Book", Author = "Cloud Author" } };
}

private async Task<List<Book>> LoadFromLocalAsync()
{
    await Task.Delay(1000); // "Disco local"
    return new List<Book> { new Book { Title = "Local Book", Author = "Local Author" } };
}

Fíjate: con await Task.WhenAll(...) ambas peticiones arrancan al mismo tiempo y se ejecutan en paralelo (si es posible), y la aplicación espera a que ambas terminen.

8. Matices útiles

Task y Fire-and-forget

A veces quieres lanzar una tarea y no esperar a que termine (p. ej. enviar logs a la nube o "tostar" algo mientras el usuario usa la app):

async void LogToCloudAsync(string message)
{
    await Task.Run(() =>
    {
        // Envío largo de log
        Thread.Sleep(1000);
        Console.WriteLine($"Log enviado: {message}");
    });
}

Pero recuerda: si ocurre un error en esa tarea será difícil enterarse. Si puedes, devuelve Task y al menos registra las excepciones dentro.

Task y Task<TResult> en la vida real

  • En apps cliente UWP/WPF/WinForms no bloquees la UI — usa Task para operaciones largas (archivos, red).
  • En WebAPI/ASP.NET Task ayuda a no desperdiciar hilos mientras se espera red/BD, mejorando el rendimiento.
  • Organiza ejecución "paralela": descargar, procesar y guardar al mismo tiempo.
  • Casi todos los métodos largos tienen versión Async: File.ReadAllTextAsync, HttpClient.GetStringAsync, etc.

FAQ y puntos inesperados

Pregunta: ¿Por qué a veces Task se ejecuta de forma síncrona?
Respuesta: Si la operación ya está completada (p. ej. el resultado está cacheado), el compilador y/o el scheduler pueden completar el método en el mismo hilo de forma síncrona. Es normal y acelera llamadas repetidas.

Pregunta: ¿Por qué no usar async void?
Respuesta: Ese método no se puede "esperar", no puedes atrapar sus errores ni seguir su finalización. Usa Task, y async void solo para EventHandler (p. ej. Button_Click).

Pregunta: ¿Se pueden lanzar varias tareas y esperar solo a una?
Respuesta: Sí — usa Task.WhenAny.

Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION