Dominio de C# Async y Await
La programación asíncrona puede parecer desalentadora por su compleja terminología, pero ofrece importantes ventajas a la hora de agilizar y mejorar la capacidad de respuesta de las aplicaciones. El vídeo de Tim Corey sobre "C# Async / Await - Make your app more responsive and faster with asynchronous programming" ofrece una guía práctica sobre el uso eficaz de estas funciones.
En este artículo, utilizaremos el detallado vídeo de Tim Corey para desglosar estos conceptos, demostrando las aplicaciones prácticas de async y await mediante ejemplos claros. Cubriremos varios escenarios, desde manejadores de eventos asíncronos hasta el manejo de múltiples tareas, contextos de sincronización, manejo de errores y los errores más comunes como el uso de métodos async void. Al final de este artículo, tendrás una sólida comprensión de la programación asíncrona y de cómo implementarla en tus propias aplicaciones de C#.
Introducción
La programación asíncrona en C# es esencial para mejorar el rendimiento de las aplicaciones, especialmente cuando se trabaja con operaciones ligadas a la E/S o a la CPU. Las palabras clave async y await permiten a los desarrolladores escribir código asincrónico que puede ejecutar tareas de larga duración sin bloquear el hilo principal. Al marcar un método con la palabra clave async, indicas que el método realizará una operación asincrónica, devolviendo un Task o Task<t>. La palabra clave await se utiliza dentro de un método asincrónico para pausar la ejecución hasta que la tarea esperada se complete, permitiendo que otras operaciones se ejecuten concurrentemente.
A diferencia de la programación síncrona, en la que las tareas se ejecutan secuencialmente y pueden bloquear el hilo de llamada, los métodos asíncronos permiten gestionar varias tareas simultáneamente, como procesos en segundo plano o la gestión de la entrada del usuario, sin congelar el hilo de la interfaz de usuario. En este artículo, exploraremos cómo async y await funcionan juntos, cubriendo conceptos como métodos void asincrónicos, manejo de errores, cambios de contexto y contextos de sincronización. Aprenderá a gestionar eficazmente múltiples operaciones asíncronas y a manejar excepciones, garantizando una ejecución fluida entre el subproceso de llamada, el subproceso de interfaz de usuario y la finalización de la tarea. Este enfoque es especialmente útil para situaciones relacionadas con sistemas de archivos, solicitudes de red o solicitudes simultáneas en las que la capacidad de respuesta es clave.
Tim introduce la programación asíncrona explicando la diferencia entre operaciones síncronas y asíncronas. En la programación síncrona, las tareas se realizan de forma secuencial, lo que provoca retrasos si una tarea lleva mucho tiempo. La programación asíncrona permite que las tareas se ejecuten en paralelo o en segundo plano, lo que mejora el rendimiento y la capacidad de respuesta.
Beneficios de la programación asíncrona
Tim destaca dos ventajas principales de la programación asíncrona:
-
Mejora de la capacidad de respuesta de la interfaz de usuario: al realizar tareas de forma asíncrona, la interfaz de usuario mantiene su capacidad de respuesta incluso cuando se ejecutan operaciones de larga duración.
- Ejecución en paralelo: las tareas independientes pueden ejecutarse en paralelo, lo que reduce el tiempo total necesario para completarlas.
Paseo por la aplicación demo
Tim crea una aplicación de demostración de Windows Presentation Foundation (WPF) para mostrar la diferencia entre operaciones síncronas y asíncronas. La aplicación tiene dos botones: uno para ejecutar tareas de forma sincrónica y otro para ejecutarlas de forma asincrónica.

La aplicación también incluye un panel de resultados para mostrar el resultado.
Operación sincrónica
Tim explica el código que hay detrás del funcionamiento síncrono. Un Stopwatch se utiliza para medir el tiempo de ejecución de la tarea.
private void ExecuteSync_Click(object sender, RoutedEventArgs e)
{
Stopwatch stopwatch = Stopwatch.StartNew();
List<string> websites = GetWebsiteList();
foreach (var website in websites)
{
string result = DownloadWebsite(website);
ReportWebsiteInfo(website, result);
}
stopwatch.Stop();
ResultsWindow.Text += $"Total execution time: {stopwatch.ElapsedMilliseconds} ms{Environment.NewLine}";
}
private List<string> GetWebsiteList()
{
return new List<string>
{
"https://www.yahoo.com",
"https://www.google.com",
"https://www.microsoft.com",
"https://www.cnn.com",
"https://www.codeproject.com",
"https://www.stackoverflow.com"
};
}
private string DownloadWebsite(string websiteURL)
{
WebClient client = new WebClient();
return client.DownloadString(websiteURL);
}
private void ReportWebsiteInfo(string website, string data)
{
ResultsWindow.Text += $"{website}: {data.Length} characters{Environment.NewLine}";
}
private void ExecuteSync_Click(object sender, RoutedEventArgs e)
{
Stopwatch stopwatch = Stopwatch.StartNew();
List<string> websites = GetWebsiteList();
foreach (var website in websites)
{
string result = DownloadWebsite(website);
ReportWebsiteInfo(website, result);
}
stopwatch.Stop();
ResultsWindow.Text += $"Total execution time: {stopwatch.ElapsedMilliseconds} ms{Environment.NewLine}";
}
private List<string> GetWebsiteList()
{
return new List<string>
{
"https://www.yahoo.com",
"https://www.google.com",
"https://www.microsoft.com",
"https://www.cnn.com",
"https://www.codeproject.com",
"https://www.stackoverflow.com"
};
}
private string DownloadWebsite(string websiteURL)
{
WebClient client = new WebClient();
return client.DownloadString(websiteURL);
}
private void ReportWebsiteInfo(string website, string data)
{
ResultsWindow.Text += $"{website}: {data.Length} characters{Environment.NewLine}";
}
Este código realiza los siguientes pasos:
- Iniciar el cronómetro: Mide el tiempo de ejecución.
- Obtener la lista de sitios web: Recupera una lista de URL de sitios web.
- Descarga cada sitio web: Descarga el contenido de cada sitio web.
- Información del sitio web: Muestra la URL y la longitud del contenido descargado.
- Stop the Stopwatch: Detiene el temporizador e informa del tiempo total de ejecución.
Observación de la operación síncrona
Tim ejecuta la aplicación para demostrar el funcionamiento síncrono. Señala que la interfaz de usuario deja de responder mientras se descargan los sitios web y que los resultados se muestran todos a la vez una vez finalizadas las descargas.

Para una demostración práctica de que el formulario no se mueve, vea el vídeo para una mejor comprensión en el minuto 9:20.
Creación de una tarea asíncrona
Tim aborda el primer problema convirtiendo el método síncrono en asíncrono. Esto implica usar las palabras clave async y await. A continuación se explica el proceso paso a paso:
-
Copiar método sincrónico existente:
- Tim copia el método síncrono existente y le cambia el nombre para indicar que será asíncrono.
private async Task RunDownloadAsync() { // Same code as RunDownloadSync, but will be modified for async }private async Task RunDownloadAsync() { // Same code as RunDownloadSync, but will be modified for async } -
Modificar llamada de descarga para ejecución asíncrona:
- Tim envuelve la llamada de descarga en un
Task.Runpara ejecutarla asincrónicamente.
private async Task<WebsiteDataModel> DownloadWebsiteAsync(string websiteURL) { return await Task.Run(() => DownloadWebsite(websiteURL)); }private async Task<WebsiteDataModel> DownloadWebsiteAsync(string websiteURL) { return await Task.Run(() => DownloadWebsite(websiteURL)); }- La palabra clave
awaitasegura que el método espere a que la tarea asincrónica se complete antes de continuar.
- Tim envuelve la llamada de descarga en un
-
Asegúrate de que el método es asíncrono:
- La firma del método se actualiza para incluir la palabra clave
asyncy devuelve un Task.
private async Task RunDownloadAsync() { List<string> websites = GetWebsiteList(); foreach (var website in websites) { var result = await DownloadWebsiteAsync(website); ReportWebsiteInfo(website, result); } }private async Task RunDownloadAsync() { List<string> websites = GetWebsiteList(); foreach (var website in websites) { var result = await DownloadWebsiteAsync(website); ReportWebsiteInfo(website, result); } } - La firma del método se actualiza para incluir la palabra clave
-
Manejar el evento correctamente:
- El controlador de eventos de clic de botón se actualiza para llamar al nuevo método asíncrono.
private async void ExecuteAsync_Click(object sender, RoutedEventArgs e) { Stopwatch stopwatch = Stopwatch.StartNew(); await RunDownloadAsync(); stopwatch.Stop(); ResultsWindow.Text += $"Total execution time: {stopwatch.ElapsedMilliseconds} ms{Environment.NewLine}"; }private async void ExecuteAsync_Click(object sender, RoutedEventArgs e) { Stopwatch stopwatch = Stopwatch.StartNew(); await RunDownloadAsync(); stopwatch.Stop(); ResultsWindow.Text += $"Total execution time: {stopwatch.ElapsedMilliseconds} ms{Environment.NewLine}"; }- Tenga en cuenta que los manejadores de eventos pueden devolver void aunque llamen a métodos asíncronos.
Atención a la capacidad de respuesta de la interfaz de usuario
Tim muestra que al usar la palabra clave await, la interfaz de usuario permanece receptiva. Esto permite a los usuarios interactuar con la ventana mientras la tarea asincrónica se está ejecutando.
// UI remains responsive
private async void ExecuteAsync_Click(object sender, RoutedEventArgs e)
{
Stopwatch stopwatch = Stopwatch.StartNew();
await RunDownloadAsync();
stopwatch.Stop();
ResultsWindow.Text += $"Total execution time: {stopwatch.ElapsedMilliseconds} ms{Environment.NewLine}";
}
// UI remains responsive
private async void ExecuteAsync_Click(object sender, RoutedEventArgs e)
{
Stopwatch stopwatch = Stopwatch.StartNew();
await RunDownloadAsync();
stopwatch.Stop();
ResultsWindow.Text += $"Total execution time: {stopwatch.ElapsedMilliseconds} ms{Environment.NewLine}";
}
Asegurar la sincronización correcta
Para asegurar que el tiempo total de ejecución se informe correctamente, Tim añade la palabra clave await a la llamada en el manejador del evento de clic en el botón.
// Correctly waits for the asynchronous task to complete
private async void ExecuteAsync_Click(object sender, RoutedEventArgs e)
{
Stopwatch stopwatch = Stopwatch.StartNew();
await RunDownloadAsync();
stopwatch.Stop();
ResultsWindow.Text += $"Total execution time: {stopwatch.ElapsedMilliseconds} ms{Environment.NewLine}";
}
// Correctly waits for the asynchronous task to complete
private async void ExecuteAsync_Click(object sender, RoutedEventArgs e)
{
Stopwatch stopwatch = Stopwatch.StartNew();
await RunDownloadAsync();
stopwatch.Stop();
ResultsWindow.Text += $"Total execution time: {stopwatch.ElapsedMilliseconds} ms{Environment.NewLine}";
}
Al esperar a que se complete el método asíncrono, el tiempo de ejecución se mide con precisión y los resultados se muestran a medida que se recuperan.
Creación de Parallel Async
Tim aborda la limitación de esperar a que cada tarea se complete secuencialmente mediante el uso de la ejecución en paralelo. He aquí cómo modifica el código para conseguirlo:
-
Copiar el método asíncrono existente:
- Tim duplica el método async existente y le cambia el nombre para indicar la ejecución paralela.
private async Task RunDownloadParallelAsync() { // Parallel execution logic will be added here }private async Task RunDownloadParallelAsync() { // Parallel execution logic will be added here } -
Crear una lista de tareas:
- Se crea una lista de tareas para almacenar todas las tareas de descarga.
List<Task<WebsiteDataModel>> tasks = new List<Task<WebsiteDataModel>>();List<Task<WebsiteDataModel>> tasks = new List<Task<WebsiteDataModel>>(); -
Inicia todas las tareas sin esperar:
- En lugar de esperar cada tarea de descarga inmediatamente, Tim las añade a la lista de tareas.
foreach (var website in websites) { tasks.Add(DownloadWebsiteAsync(website)); }foreach (var website in websites) { tasks.Add(DownloadWebsiteAsync(website)); } -
Espere a que se completen todas las tareas:
- Se utiliza el método
Task.WhenAllpara esperar la finalización de todas las tareas. Este método devuelve una matriz de resultados una vez completadas todas las tareas.
WebsiteDataModel[] results = await Task.WhenAll(tasks);WebsiteDataModel[] results = await Task.WhenAll(tasks); - Se utiliza el método
-
Procesamiento de los resultados:
- Una vez completadas todas las tareas, Tim procesa los resultados en un bucle.
foreach (var result in results) { ReportWebsiteInfo(result); }foreach (var result in results) { ReportWebsiteInfo(result); }
Aquí está el código completo para el método de ejecución en paralelo:
private async Task RunDownloadParallelAsync()
{
List<string> websites = GetWebsiteList();
List<Task<WebsiteDataModel>> tasks = new List<Task<WebsiteDataModel>>();
foreach (var website in websites)
{
tasks.Add(DownloadWebsiteAsync(website));
}
WebsiteDataModel[] results = await Task.WhenAll(tasks);
foreach (var result in results)
{
ReportWebsiteInfo(result);
}
}
private async Task RunDownloadParallelAsync()
{
List<string> websites = GetWebsiteList();
List<Task<WebsiteDataModel>> tasks = new List<Task<WebsiteDataModel>>();
foreach (var website in websites)
{
tasks.Add(DownloadWebsiteAsync(website));
}
WebsiteDataModel[] results = await Task.WhenAll(tasks);
foreach (var result in results)
{
ReportWebsiteInfo(result);
}
}
Actualización del controlador de eventos
Tim actualiza el controlador de eventos de clic del botón para llamar al nuevo método de ejecución paralela.
private async void ExecuteAsync_Click(object sender, RoutedEventArgs e)
{
Stopwatch stopwatch = Stopwatch.StartNew();
await RunDownloadParallelAsync();
stopwatch.Stop();
ResultsWindow.Text += $"Total execution time: {stopwatch.ElapsedMilliseconds} ms{Environment.NewLine}";
}
private async void ExecuteAsync_Click(object sender, RoutedEventArgs e)
{
Stopwatch stopwatch = Stopwatch.StartNew();
await RunDownloadParallelAsync();
stopwatch.Stop();
ResultsWindow.Text += $"Total execution time: {stopwatch.ElapsedMilliseconds} ms{Environment.NewLine}";
}
Al esperar a RunDownloadParallelAsync, el tiempo total de ejecución se mide e informa con precisión.
Observando la mejora del rendimiento
Tim demuestra la mejora del rendimiento ejecutando la aplicación en paralelo. Los resultados muestran una reducción significativa del tiempo total de ejecución en comparación con la ejecución secuencial.
// Parallel execution
async Task RunDownloadParallelAsync();
// Parallel execution
async Task RunDownloadParallelAsync();
El aumento de velocidad es evidente a medida que los sitios web se descargan simultáneamente, reduciendo el tiempo total de espera a la duración de la descarga individual más lenta.
Envolviendo el método en Task.Run() vs Llamada de método asincrónico
Tim explica el concepto de usar Task.Run() para envolver un método sincrónico y hacerlo asincrónico cuando no puedes modificar el método original. Sin embargo, también muestra el enfoque preferido de modificar el propio método para que sea asíncrono si se tiene control sobre él.
-
Envolviendo el método en
Task.Run():- Este enfoque es útil cuando no se puede cambiar el código del método original pero se desea ejecutarlo de forma asíncrona.
private async Task<WebsiteDataModel> DownloadWebsiteAsync(string websiteURL) { return await Task.Run(() => DownloadWebsite(websiteURL)); }private async Task<WebsiteDataModel> DownloadWebsiteAsync(string websiteURL) { return await Task.Run(() => DownloadWebsite(websiteURL)); } -
Cómo hacer que un método sea asíncrono:
-
Si puede modificar el método, es mejor convertir el propio método para que sea asíncrono utilizando una API asíncrona adecuada.
- Tim demuestra esto cambiando
DownloadWebsiteaDownloadWebsiteAsyncy usandoDownloadStringTaskAsyncdeWebClient.
private async Task<WebsiteDataModel> DownloadWebsiteAsync(string websiteURL) { WebClient client = new WebClient(); string data = await client.DownloadStringTaskAsync(websiteURL); return new WebsiteDataModel { URL = websiteURL, Data = data }; }private async Task<WebsiteDataModel> DownloadWebsiteAsync(string websiteURL) { WebClient client = new WebClient(); string data = await client.DownloadStringTaskAsync(websiteURL); return new WebsiteDataModel { URL = websiteURL, Data = data }; } -
-
Ajuste del método de llamada:
- Después de convertir el método, el código que llama necesita ser ajustado para eliminar el envoltorio
Task.Run()y llamar directamente al método asincrónico.
private async Task RunDownloadParallelAsync() { List<string> websites = GetWebsiteList(); List<Task<WebsiteDataModel>> tasks = new List<Task<WebsiteDataModel>>(); foreach (var website in websites) { tasks.Add(DownloadWebsiteAsync(website)); } WebsiteDataModel[] results = await Task.WhenAll(tasks); foreach (var result in results) { ReportWebsiteInfo(result); } }private async Task RunDownloadParallelAsync() { List<string> websites = GetWebsiteList(); List<Task<WebsiteDataModel>> tasks = new List<Task<WebsiteDataModel>>(); foreach (var website in websites) { tasks.Add(DownloadWebsiteAsync(website)); } WebsiteDataModel[] results = await Task.WhenAll(tasks); foreach (var result in results) { ReportWebsiteInfo(result); } } - Después de convertir el método, el código que llama necesita ser ajustado para eliminar el envoltorio
Resumen de puntos clave
Tim concluye con varios consejos importantes sobre el uso de async y await:
-
Asegurar métodos que devuelven tareas: Siempre que marques un método como
async, debe devolver unTaskoTask<t>en lugar de void (excepto para los manejadores de eventos). -
Usar
awaitpara operaciones confiables: Usa la palabra claveawaitcuando necesites el resultado de una operación asincrónica antes de continuar. -
Envoltura de tarea para código no modificable: Usa
Task.Run()para envolver métodos sincrónicos cuando no puedes modificar el método original. -
Marcar métodos asincrónicos adecuadamente: Siempre añade
Asyncal nombre del método para indicar que es una operación asincrónica. - Ejecución en paralelo frente a ejecución secuencial: Decidir si las tareas pueden ejecutarse en paralelo o deben esperarse unas a otras en función de las dependencias entre tareas.
Tim enfatiza que la programación asincrónica en C# se ha simplificado con async, await, y Task. Las complejidades como los contextos de hilos y los modelos de apartamentos se gestionan entre bastidores.
Conclusión
El tutorial de Tim Corey sobre async y await en C# ofrece un recurso inestimable para dominar la programación asíncrona. Al explicar cuidadosamente conceptos como el paralelismo de tareas, la capacidad de respuesta de la interfaz de usuario y la importancia de utilizar async y await en el contexto adecuado, Tim proporciona a los desarrolladores las herramientas necesarias para crear aplicaciones más rápidas y con mayor capacidad de respuesta. Su detallado recorrido demuestra cómo gestionar eficazmente las tareas de larga duración sin bloquear el hilo principal, así como las estrategias para gestionar las operaciones asíncronas y la ejecución en paralelo.
Para una comprensión más profunda y ejemplos prácticos adicionales, recomiendo encarecidamente ver el vídeo de Tim Corey. Sus conocimientos sobre los métodos async void, el tratamiento de errores y la gestión de contextos de sincronización mejorarán aún más su capacidad para implementar estos conceptos en sus proyectos.
