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 |
|---|---|
|
Estado actual de la tarea |
|
Resultado para Task<TResult> (bloquea el hilo) |
|
Si la tarea está completada |
|
Si ocurrió una excepción en la tarea |
|
Si la tarea fue cancelada |
|
Bloquea el hilo actual hasta completarse (peligroso) |
|
Ejecutar otra tarea después de completar |
|
Acceso a la excepción si el Task terminó con error |
|
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 | |
Operación asíncrona | Normalmente I/O, comodidad |
|
|
Tarea en background | CPU-bound (cálculos) |
|
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.
GO TO FULL VERSION