Compartir a través de


Depuración de una aplicación asincrónica

En este tutorial se muestra cómo usar la vista Tareas de la ventana Pilas paralelas para depurar una aplicación asincrónica de C#. Esta ventana le ayuda a comprender y comprobar el comportamiento en tiempo de ejecución del código que usa el patrón async/await, también denominado patrón asincrónico basado en tareas (TAP).

En el caso de las aplicaciones que usan la biblioteca paralela de tareas (TPL), pero no el patrón async/await, o para las aplicaciones de C++ mediante el runtime de simultaneidad, use la vista Subprocesos en la ventana Pilas paralelas para la depuración. Para obtener más información, vea Depurar un interbloqueo y Ver subprocesos y tareas en la ventana Pilas paralelas.

La vista Tareas le ayuda a:

  • Vea las visualizaciones del stack de llamadas para las aplicaciones que usan el patrón async/await. En estos escenarios, la vista Tareas proporciona una imagen más completa del estado de la aplicación.

  • Identifique el código asincrónico que está programado para ejecutarse, pero que aún no se está ejecutando. Por ejemplo, una solicitud HTTP que no ha devuelto ningún dato es más probable que aparezca en la vista Tareas en lugar de en la vista Subprocesos, lo que le ayuda a aislar el problema.

  • Ayuda a identificar problemas como el patrón de sincronización a través de async junto con sugerencias relacionadas con posibles problemas, como tareas bloqueadas o en espera. El patrón de código sync-over-async se refiere al código que llama a métodos asíncronos de forma síncrona, lo que se sabe que bloquea los subprocesos y es la causa más común del colapso del grupo de subprocesos.

Pilas de llamadas asíncronas

La vista de Tareas en Pilas Paralelas proporciona una visualización de las pilas de llamadas asincrónicas, para que puedas ver lo que sucede (o se supone que debe suceder) en tu aplicación.

Estos son algunos puntos importantes que recordar al interpretar los datos en la vista Tareas.

  • Las pilas de llamadas asincrónicas son pilas de llamadas lógicas o virtuales, no pilas de llamadas físicas que representan la pila. Al trabajar con código asincrónico (por ejemplo, mediante la await palabra clave ), el depurador proporciona una vista de las "pilas de llamadas asincrónicas" o "pilas de llamadas virtuales". Las pilas de llamadas asincrónicas son diferentes de las pilas de llamadas basadas en subprocesos o "pilas físicas", ya que las pilas de llamadas asincrónicas no se ejecutan necesariamente en ningún subproceso físico. En su lugar, las pilas de llamadas asincrónicas son continuaciones o "promesas" de código que se ejecutarán en el futuro, de forma asincrónica. Las pilas de llamadas se crean mediante continuaciones.

  • El código asincrónico programado pero que no se está ejecutando actualmente no aparece en la pila de llamadas físicas, pero debería aparecer en la pila de llamadas asincrónicas en la vista Tareas. Si está bloqueando subprocesos usando métodos como .Wait o .Result, es posible que vea el código en la pila de llamada física en su lugar.

  • Las pilas de llamadas virtuales asincrónicas no siempre son intuitivas, debido a la bifurcación que resulta del uso de llamadas de método como .WaitAny o .WaitAll.

  • La ventana Pila de llamadas puede ser útil en combinación con la vista Tareas, ya que muestra la pila de llamadas físicas para el subproceso en ejecución actual.

  • Las secciones idénticas de la pila de llamadas virtuales se agrupan para simplificar la visualización de aplicaciones complejas.

    La siguiente animación conceptual muestra cómo se aplica la agrupación a las pilas de llamadas virtuales. Solo se agrupan segmentos idénticos de una pila de llamadas virtuales. Mantenga el puntero sobre una pila de llamadas agrupada para identificar los subprocesos que ejecutan las tareas.

    Ilustración de la agrupación de pilas de llamadas virtuales.

Ejemplo de C#

El código de ejemplo de este tutorial es para una aplicación que simula un día en la vida de un gorila. El propósito del ejercicio es entender cómo usar la vista Tareas de la ventana Pilas paralelas para depurar una aplicación asíncrona.

La muestra incluye un ejemplo de uso del antipatrón sync-over-async, que puede provocar el colapso del grupo de subprocesos.

Para que la pila de llamadas sea intuitiva, la aplicación de ejemplo realiza los pasos secuenciales siguientes:

  1. Crea un objeto que representa un gorila.
  2. Gorila se despierta.
  3. Gorila va a caminar por la mañana.
  4. Gorilla encuentra plátanos en la selva.
  5. Gorila consume.
  6. Gorilla se involucra en el negocio de los monos.

Creación del proyecto de ejemplo

  1. Abra Visual Studio y cree un proyecto.

    Si la ventana de inicio no está abierta, elija Archivo>Iniciar ventana.

    En la ventana de inicio, elija Nuevo proyecto.

    En la ventana Crear un nuevo proyecto, escriba console en el cuadro de búsqueda. A continuación, elija de C# en la lista Lenguaje y, a continuación, elija Windows en la lista Plataforma.

    Después de aplicar los filtros de idioma y plataforma, elija la aplicación de consola para .NET y, a continuación, elija Siguiente.

    Nota:

    Si no ve la plantilla correcta, vaya a Herramientas>Obtener herramientas y características..., que abre el Instalador de Visual Studio. Elija la carga de trabajo Desarrollo de escritorio de .NET y, luego, seleccione Modificar.

    En la ventana Configurar el nuevo proyecto , escriba un nombre o use el nombre predeterminado en el cuadro Nombre del proyecto. Después, elija Siguiente.

    En .NET, elija la plataforma de destino recomendada o .NET 8 y, a continuación, elija Crear.

    Aparece un nuevo proyecto de consola. Una vez creado el proyecto, aparece un archivo de origen.

  2. Abra el archivo de código .cs en el proyecto. Elimine su contenido para crear un archivo de código vacío.

  3. Pegue el código siguiente para el idioma elegido en el archivo de código vacío.

    using System.Diagnostics;
    
    namespace AsyncTasks_SyncOverAsync
    {
         class Jungle
         {
             public static async Task<int> FindBananas()
             {
                 await Task.Delay(1000);
                 Console.WriteLine("Got bananas.");
                 return 0;
             }
    
             static async Task Gorilla_Start()
             {
                 Debugger.Break();
                 Gorilla koko = new Gorilla();
                 int result = await Task.Run(koko.WakeUp);
             }
    
             static async Task Main(string[] args)
             {
                 List<Task> tasks = new List<Task>();
                 for (int i = 0; i < 2; i++)
                 {
                     Task task = Gorilla_Start();
                     tasks.Add(task);
    
                 }
                 await Task.WhenAll(tasks);
    
             }
         }
    
         class Gorilla
         {
    
             public async Task<int> WakeUp()
             {
                 int myResult = await MorningWalk();
    
                 return myResult;
             }
    
             public async Task<int> MorningWalk()
             {
                 int myResult = await Jungle.FindBananas();
                 GobbleUpBananas(myResult);
    
                 return myResult;
             }
    
             /// <summary>
             /// Calls a .Wait.
             /// </summary>
             public void GobbleUpBananas(int food)
             {
                 Console.WriteLine("Trying to gobble up food synchronously...");
    
                 Task mb = DoSomeMonkeyBusiness();
                 mb.Wait();
    
             }
    
             public async Task DoSomeMonkeyBusiness()
             {
                 Debugger.Break();
                 while (!System.Diagnostics.Debugger.IsAttached)
                 {
                     Thread.Sleep(100);
                 }
    
                 await Task.Delay(30000);
                 Console.WriteLine("Monkey business done");
             }
         }
    }
    

    Después de actualizar el archivo de código, guarde los cambios y compile la solución.

  4. En el menú Archivo, seleccione Guardar todo.

  5. En el menú Compilar, seleccione Compilar solución.

Usar la vista Tareas de la ventana Pilas paralelas

  1. En el menú Depurar, seleccione Iniciar depuración (o F5) y espere a que se pulse el primer Debugger.Break().

  2. Presione F5 una vez y el depurador se pausa de nuevo en la misma Debugger.Break() línea.

    Esto se detiene en la segunda llamada a Gorilla_Start, que se produce dentro de una segunda tarea asincrónica.

  3. Seleccione Depurar > Windows > Pilas paralelas para abrir la ventana Pilas paralelas y, a continuación, seleccione Tareas en el menú desplegable Vista de la ventana.

    Captura de pantalla de la vista Tareas en la ventana

    Observe que las etiquetas de las pilas de llamadas asincrónicas describen 2 pilas lógicas asincrónicas. Al presionar F5 por última vez, inició otra tarea. Para simplificar en aplicaciones complejas, las pilas de llamadas asincrónicas idénticas se agrupan en una sola representación visual. Esto proporciona información más completa, especialmente en escenarios con muchas tareas.

    A diferencia de la vista Tareas, la ventana Pila de llamadas muestra la pila de llamadas solo para el subproceso actual, no para varias tareas. A menudo resulta útil ver ambos juntos para obtener una imagen más completa del estado de la aplicación.

    Captura de pantalla de la pila de llamadas.

    Sugerencia

    La ventana Pila de llamadas puede mostrarle información como, por ejemplo, un punto muerto, mediante la descripción Async cycle.

    Durante la depuración, puede alternar si se muestra el código externo. Para alternar la característica, haga clic con el botón derecho en el encabezado de la tabla Nombre de la ventana Pila de llamadas y, a continuación, seleccione o desactive Mostrar código externo. Si muestra código externo, puede seguir usando este tutorial, pero los resultados pueden diferir de las ilustraciones.

  4. Presione F5 de nuevo y el depurador se pausa en el método DoSomeMonkeyBusiness.

    Captura de pantalla de la vista Tareas después de F5.

    Esta vista muestra una pila de llamadas asincrónicas más completa después de agregar más métodos asincrónicos a la cadena de continuación interna, que se produce al usar await y métodos similares. DoSomeMonkeyBusiness puede estar presente o no en la parte superior de la pila de llamadas asíncronas porque es un método asincrónico, pero aún no se ha agregado a la cadena de continuación. Exploraremos por qué este es el caso en los pasos que se indican a continuación.

    Esta vista también muestra el icono bloqueado de Jungle.MainEstado bloqueado. Esto es informativo, pero normalmente no indica un problema. Una tarea bloqueada es una que está bloqueada porque está esperando a que finalice otra tarea, un evento que se va a indicar o un bloqueo que se va a liberar.

  5. Mantenga el puntero sobre el GobbleUpBananas método para obtener información sobre los dos subprocesos que ejecutan las tareas.

    Captura de pantalla de los subprocesos asociados a la pila de llamadas.

    El subproceso actual también aparece en la lista Subproceso de la barra de herramientas de depuración.

    Captura de pantalla del subproceso actual en la barra de herramientas de depuración.

    Puede usar la lista Subprocesos para cambiar el contexto del depurador a otro subproceso.

  6. Pulse F5 de nuevo y el depurador se detendrá en el método DoSomeMonkeyBusiness para la segunda tarea.

    Captura de pantalla de la vista Tareas después de la segunda F5.

    Según el tiempo de ejecución de la tarea, en este momento verá pilas de llamadas asincrónicas independientes o agrupadas.

    En la ilustración anterior, las pilas de llamadas asincrónicas para las dos tareas son independientes porque no son idénticas.

  7. Presione F5 de nuevo y verá que se produce un retraso largo y la vista Tareas no muestra ninguna información de pila de llamadas asincrónica.

    El retraso se debe a una tarea de ejecución prolongada. Para los propósitos de este ejemplo, simula una tarea de larga duración, como una solicitud web, que puede dar lugar a un caso de colapso del grupo de subprocesos. No aparece nada en la vista Tareas porque, aunque las tareas puedan estar bloqueadas, actualmente no está en pausa en el depurador.

    Sugerencia

    El botón Interrumpir todo es una buena manera de obtener información de la pila de llamadas si se produce un interbloqueo o si todas las tareas y subprocesos están bloqueados.

  8. En la parte superior del IDE de la barra de herramientas Depurar, seleccione el botón Interrumpir todo (icono de pausa), Ctrl + Alt + Interrumpir.

    Captura de pantalla de la vista Tareas después de seleccionar Interrumpir todo.

    Cerca de la parte superior de la pila de llamadas asíncronas en la vista Tareas, puede ver que GobbleUpBananas está bloqueado. De hecho, se bloquean dos tareas en el mismo punto. Una tarea bloqueada no es necesariamente inesperada y no significa necesariamente que haya un problema. Sin embargo, el retraso observado en la ejecución indica un problema y la información de la pila de llamadas aquí muestra la ubicación del problema.

    En el lado izquierdo de la captura de pantalla anterior, la flecha verde curvada indica el contexto actual del depurador. Las dos tareas se bloquean en mb.Wait() en el método GobbleUpBananas.

    La ventana Pila de llamadas también muestra que el subproceso actual está bloqueado.

    Captura de pantalla de la pila de llamadas después de seleccionar Interrumpir todo.

    La llamada a Wait() bloquea los hilos durante la ejecución sincrónica de la llamada a GobbleUpBananas. Este es un ejemplo del antipatrón sync-over-async, y si esto ocurriera en un subproceso de UI o bajo grandes cargas de trabajo de procesamiento, normalmente se solucionaría con una corrección de código usando await. Para obtener más información, consulte Depuración del colapso del grupo de subprocesos. Si desea usar herramientas de generación de perfiles para la depuración del colapso del grupo de subprocesos, consulte Caso de estudio: Aislar un problema de rendimiento.

    También de interés, DoSomeMonkeyBusiness no aparece en la pila de llamadas. Actualmente está programado, no en ejecución, por lo que solo aparece en la pila de llamadas asincrónicas en la vista Tareas.

    Sugerencia

    El depurador interrumpe el código por subprocesos. Por ejemplo, esto significa que si presiona F5 para continuar la ejecución y la aplicación alcanza el siguiente punto de interrupción, puede dividirse en código en un subproceso diferente. Si necesita administrarlo con fines de depuración, puede agregar puntos de interrupción adicionales, agregar puntos de interrupción condicionales o usar Interrumpir todo. Para obtener más información sobre este comportamiento, vea Seguir un único subproceso con puntos de interrupción condicionales.

Corrección del código de ejemplo

  1. Reemplace el método GobbleUpBananas por el código siguiente.

     public async Task GobbleUpBananas(int food) // Previously returned void.
     {
         Console.WriteLine("Trying to gobble up food...");
    
         //Task mb = DoSomeMonkeyBusiness();
         //mb.Wait();
         await DoSomeMonkeyBusiness();
     }
    
  2. En el MorningWalk método , llame a GobbleUpBananas mediante await.

    await GobbleUpBananas(myResult);
    
  3. Seleccione el botón Reiniciar (Ctrl + Mayús + F5), y luego presione F5 varias veces hasta que la aplicación parezca "congelarse".

  4. Pulse Interrumpir todo.

    Esta vez, GobbleUpBananas se ejecuta de forma asincrónica. Cuando se interrumpe, se ve la pila de llamadas asíncronas.

    Captura de pantalla del contexto del depurador después de la corrección de código.

    La ventana Pila de llamadas está vacía, excepto para la ExternalCode entrada.

    El editor de código no nos muestra nada, salvo que proporciona un mensaje que indica que todos los subprocesos ejecutan código externo.

    Sin embargo, la vista Tareas proporciona información útil. DoSomeMonkeyBusiness está en la parte superior de la pila de llamadas asíncronas, como era de esperar. Esto nos indica correctamente dónde se encuentra el método de larga duración. Esto es útil para aislar problemas de async/await cuando la pila de llamadas física en la ventana Pila de llamadas no proporciona suficientes detalles.

Resumen

En este tutorial se ha mostrado la ventana del depurador Pilas paralelas. Use esta ventana en las aplicaciones que usan el patrón async/await.