Compartir a través de


Depurar un interbloqueo utilizando la vista Subprocesos

En este tutorial se muestra cómo usar la vista Subprocesos de las ventanas Pilas paralelas para depurar una aplicación multiproceso. Esta ventana le ayuda a comprender y comprobar el comportamiento en tiempo de ejecución del código multiproceso.

La vista Subprocesos es compatible con C#, C++y Visual Basic. El código de ejemplo se proporciona para C# y C++, pero algunas de las referencias de código y las ilustraciones solo se aplican al código de ejemplo de C#.

La vista Subprocesos le ayuda a:

  • Vea visualizaciones de pila de llamadas para varios subprocesos, que proporciona una imagen más completa del estado de la aplicación que la ventana Pila de llamadas, que solo muestra la pila de llamadas para el subproceso actual.

  • Identificar problemas como subprocesos bloqueados o interbloqueos.

Pilas de llamadas multiproceso

Las secciones idénticas de la pila de llamadas 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. Solo se agrupan segmentos idénticos de una pila de llamadas. Mantenga el puntero sobre un conjunto de pilas de llamadas para identificar los subprocesos.

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

Introducción al código de ejemplo (C#, 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 comprender cómo usar la vista Subprocesos de la ventana Pilas paralelas para depurar una aplicación multiproceso.

La muestra incluye un caso de interbloqueo, que se produce cuando dos subprocesos esperan el uno al otro.

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

Para crear el proyecto:

  1. Abra Visual Studio y cree un proyecto.

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

    En la ventana Inicio, elija Nuevo proyecto.

    En la ventana Crear un nuevo proyecto, escriba console en el cuadro de búsqueda. A continuación, elija C# o 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 el idioma elegido y, a continuación, elija Siguiente.

    Note

    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.

    Para un proyecto de .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 (o .cpp) 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 Multithreaded_Deadlock
     {
         class Jungle
         {
             public static readonly object tree = new object();
             public static readonly object banana_bunch = new object();
             public static Barrier barrier = new Barrier(2);
    
             public static int FindBananas()
             {
                 // Lock tree first, then banana
                 lock (tree)
                 {
                     lock (banana_bunch)
                     {
                         Console.WriteLine("Got bananas.");
                         return 0;
                     }
                 }
             }
    
             static void Gorilla_Start(object lockOrderObj)
             {
                 Debugger.Break();
                 bool lockTreeFirst = (bool)lockOrderObj;
                 Gorilla koko = new Gorilla(lockTreeFirst);
                 int result = 0;
                 var done = new ManualResetEventSlim(false);
    
                 Thread t = new Thread(() =>
                 {
                     result = koko.WakeUp();
                     done.Set();
                 });
                 t.Start();
                 done.Wait();
             }
    
             static void Main(string[] args)
             {
                 List<Thread> threads = new List<Thread>();
                 // Start two threads with opposite lock orders
                 threads.Add(new Thread(Gorilla_Start));
                 threads[0].Start(true);  // First gorilla locks tree then banana
                 threads.Add(new Thread(Gorilla_Start));
                 threads[1].Start(false); // Second gorilla locks banana then tree
    
                 foreach (var t in threads)
                 {
                     t.Join();
                 }
             }
         }
    
         class Gorilla
         {
             private readonly bool lockTreeFirst;
    
             public Gorilla(bool lockTreeFirst)
             {
                 this.lockTreeFirst = lockTreeFirst;
             }
    
             public int WakeUp()
             {
                 int myResult = MorningWalk();
                 return myResult;
             }
    
             public int MorningWalk()
             {
                 Debugger.Break();
                 if (lockTreeFirst)
                 {
                     lock (Jungle.tree)
                     {
                         Jungle.barrier.SignalAndWait(5000); // For thread timing consistency in sample
                         Jungle.FindBananas();
                         GobbleUpBananas();
                     }
                 }
                 else
                 {
                     lock (Jungle.banana_bunch)
                     {
                         Jungle.barrier.SignalAndWait(5000); // For thread timing consistency in sample
                         Jungle.FindBananas();
                         GobbleUpBananas();
                     }
                 }
                 return 0;
             }
    
             public void GobbleUpBananas()
             {
                 Console.WriteLine("Trying to gobble up food...");
                 DoSomeMonkeyBusiness();
             }
    
             public void DoSomeMonkeyBusiness()
             {
                 Thread.Sleep(1000);
                 Console.WriteLine("Monkey business done");
             }
         }
     }
    
  4. En el menú Archivo, seleccione Guardar todo.

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

Utilizar la vista Subprocesos de la ventana Pilas paralelas

Para iniciar la depuración:

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

    Note

    En C++, el depurador se detiene en __debug_break(). El resto de las referencias e ilustraciones de código de este artículo son para la versión de C#, pero los mismos principios de depuración se aplican a C++.

  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 un segundo subproceso.

    Tip

    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 administrar este comportamiento con fines de depuración, puede agregar puntos de interrupción adicionales, puntos de interrupción condicionales o usar Interrumpir todo. Para obtener más información sobre el uso de puntos de interrupción condicionales, vea Seguir un único subproceso con puntos de interrupción condicionales.

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

    Captura de pantalla de la vista de subprocesos en la ventana de pilas paralelas.

    En la vista Subprocesos, el marco de pila y la ruta de acceso de llamada del subproceso actual se resaltan en azul. La ubicación actual del hilo se muestra mediante la flecha amarilla.

    Observe que la etiqueta de la pila de llamadas para Gorilla_Start es 2 Subprocesos. Al presionar F5 por última vez, ha iniciado otro subproceso. Para simplificar en aplicaciones complejas, las pilas de llamadas idénticas se agrupan en una sola representación visual. Esto simplifica la información potencialmente compleja, especialmente en escenarios con muchos subprocesos.

    Durante la depuración, puede alternar si se muestra el código externo. Para alternar la característica, 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 detiene en la línea Debugger.Break() del método MorningWalk.

    La ventana Pilas paralelas muestra la ubicación del subproceso de ejecución actual en el método MorningWalk.

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

  5. Mantenga el puntero sobre el método MorningWalk para obtener información sobre los dos subprocesos representados por la pila de llamadas agrupada.

    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. Esto no cambia el subproceso en ejecución actual, solo el contexto del depurador.

    Como alternativa, puede cambiar el contexto del depurador haciendo doble clic en un método en la vista Subprocesos o haciendo clic con el botón derecho en un método en la vista Subprocesos y seleccionando Cambiar a Marco>[id. de subproceso].

  6. Pulse F5 de nuevo y el depurador se detendrá en el método MorningWalk para el segundo subproceso.

    Captura de pantalla de la vista de subprocesos tras pulsar F5 por segunda vez.

    En función del tiempo de ejecución del subproceso, en este momento verá pilas de llamadas independientes o agrupadas.

    En la ilustración anterior, las pilas de llamadas de los dos hilos se agrupan parcialmente. Los segmentos idénticos de las pilas de llamadas se agrupan y las líneas de flecha apuntan a los segmentos separados (es decir, no idénticos). El marco de pila actual se indica mediante el resaltado azul.

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

    El retraso se debe a un interbloqueo. No aparece nada en la vista Subprocesos porque, aunque los subprocesos puedan estar bloqueados, actualmente no está en pausa en el depurador.

    Note

    En C++, también verá un error de depuración que indica que abort() se ha llamado a .

    Tip

    El botón Detener todo es una buena manera de obtener información de la pila de llamadas si se produce un interbloqueo o todos los subprocesos están bloqueados actualmente.

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

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

    La parte superior de la pila de llamadas de la vista Subprocesos muestra que FindBananas está interbloqueada. El puntero de ejecución de FindBananas es una flecha verde curvada, que indica el contexto actual del depurador, pero también nos indica que los subprocesos no se están ejecutando actualmente.

    Note

    En C++, no ve la información de "interbloqueo detectada" útil e iconos. Sin embargo, todavía encuentra la flecha verde curlada en Jungle.FindBananas, que indica en la ubicación del interbloqueo.

    En el editor de código, encontramos la flecha verde curvada en la lock función . Los dos subprocesos están bloqueados en la función lock del método FindBananas.

    Captura de pantalla del editor de código después de seleccionar Interrumpir todo.

    Dependiendo del orden de ejecución del subproceso, el interbloqueo aparece en la instrucción lock(tree) o lock(banana_bunch).

    La llamada a lock bloquea los subprocesos del FindBananas método . Un subproceso espera a que el otro subproceso libere el bloqueo tree , pero el otro subproceso espera a que se libere el bloqueo banana_bunch antes de que pueda liberar el bloqueo en tree. Este es un ejemplo de un interbloqueo clásico que ocurre cuando dos subprocesos están esperando el uno al otro.

    Si usa Copilot, también podrá obtener resúmenes generados por IA de los subprocesos para ayudar a identificar posibles interbloqueos.

    Captura de pantalla de descripciones de resúmenes de los subprocesos de Copilot.

Corrección del código de ejemplo

Para corregir este código, adquiera siempre varios cerrojos en un orden global coherente en todos los subprocesos. Esto evita esperas circulares y elimina interbloqueos.

  1. Para corregir el interbloqueo, reemplace el código de MorningWalk por el código siguiente.

    public int MorningWalk()
    {
        Debugger.Break();
        // Always lock tree first, then banana_bunch
        lock (Jungle.tree)
        {
            Jungle.barrier.SignalAndWait(5000); // OK to remove
            lock (Jungle.banana_bunch)
            {
                Jungle.FindBananas();
                GobbleUpBananas();
            }
        }
        return 0;
    }
    
  2. Reinicie la aplicación.

Summary

En este tutorial se ha mostrado la ventana del depurador Pilas paralelas. Use esta ventana en proyectos reales que usan código multiproceso. Puede examinar el código paralelo escrito en C++, C#o Visual Basic.