Bagikan melalui


Memecahkan kebuntuan menggunakan tampilan Threads

Tutorial ini menunjukkan cara menggunakan tampilan Utas jendela Parallel Stacks untuk men-debug aplikasi multithread. Jendela ini membantu Anda memahami dan memverifikasi perilaku run-time kode multithreaded.

Tampilan Utas didukung untuk C#, C++, dan Visual Basic. Kode sampel disediakan untuk C# dan C++, tetapi beberapa referensi kode dan ilustrasi hanya berlaku untuk kode sampel C#.

Tampilan Utas membantu Anda:

  • Lihat visualisasi tumpukan panggilan untuk beberapa utas, yang memberikan gambaran yang lebih lengkap tentang status aplikasi Anda daripada jendela Call Stack, yang hanya menampilkan tumpukan panggilan untuk utas saat ini.

  • Membantu mengidentifikasi masalah seperti utas yang diblokir atau terkunci mati.

Tumpukan panggilan berutas ganda

Bagian identik dari tumpukan panggilan dikelompokkan bersama untuk menyederhanakan visualisasi untuk aplikasi yang kompleks.

Animasi konseptual berikut menunjukkan bagaimana pengelompokan diterapkan ke tumpukan panggilan. Hanya segmen identik dari tumpukan panggilan yang dikelompokkan. Arahkan mouse ke tumpukan panggilan yang dikelompokkan untuk mengidenitfy utas.

Ilustrasi pengelompokan tumpukan panggilan.

Gambaran umum kode sampel (C#, C++)

Kode sampel dalam panduan ini adalah untuk aplikasi yang mensimulasikan sehari dalam kehidupan gorila. Tujuan latihan ini adalah untuk memahami cara menggunakan tampilan Utas dari jendela Parallel Stacks untuk men-debug aplikasi multithread.

Sampel mencakup contoh kebuntuan, yang terjadi ketika dua utas saling menunggu.

Untuk membuat tumpukan panggilan intuitif, aplikasi sampel melakukan langkah-langkah berurutan berikut:

  1. Membuat objek yang mewakili gorila.
  2. Gorila bangun.
  3. Gorila pergi berjalan pagi.
  4. Gorila menemukan pisang di hutan.
  5. Gorila makan.
  6. Gorila terlibat dalam bisnis monyet.

Membuat proyek sampel

Untuk membuat proyek:

  1. Buka Visual Studio dan buat proyek baru.

    Jika jendela Mulai tidak terbuka, pilih File>Jendela Mulai.

    Pada jendela Mulai, pilih Proyek baru.

    Pada jendela Buat proyek baru , masukkan atau ketik konsol di kotak pencarian. Selanjutnya, pilih C# atau C++ dari daftar Bahasa, lalu pilih Windows dari daftar Platform.

    Setelah Anda menerapkan filter bahasa dan platform, pilih Aplikasi Konsol untuk bahasa yang Anda pilih, lalu pilih Berikutnya.

    Note

    Jika Anda tidak melihat templat yang benar, buka Alat>Dapatkan Alat dan Fitur..., yang membuka Alat Penginstal Visual Studio. Pilih beban kerja pengembangan desktop .NET, lalu pilih Ubah.

    Di jendela Konfigurasikan proyek baru Anda , ketik nama atau gunakan nama default dalam kotak Nama proyek . Kemudian, pilih Berikutnya.

    Untuk proyek .NET, pilih kerangka kerja target yang direkomendasikan atau .NET 8, lalu pilih Buat.

    Proyek konsol baru muncul. Setelah proyek dibuat, file sumber muncul.

  2. Buka file kode .cs (atau .cpp) dalam proyek. Hapus isinya untuk membuat file kode kosong.

  3. Tempelkan kode berikut untuk bahasa yang Anda pilih ke dalam file kode kosong.

     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. Pada menu File, pilih Simpan Semua.

  5. Dari menu Buat, pilih Buat Solusi.

Menggunakan tampilan Utas dari jendela Tumpukan Paralel

Untuk memulai debugging:

  1. Pada menu Debug , pilih Mulai Penelusuran Kesalahan (atau F5) dan tunggu hingga yang pertama Debugger.Break() tertemu.

    Note

    Di C++, debugger berhenti sejenak di __debug_break(). Referensi dan ilustrasi kode lainnya dalam artikel ini adalah untuk versi C#, tetapi prinsip penelusuran kesalahan yang sama berlaku untuk C++.

  2. Tekan F5 sekali, dan debugger berhenti lagi pada baris yang sama Debugger.Break() .

    Jeda terjadi dalam panggilan kedua di Gorilla_Start, yang terjadi di utas kedua.

    Tip

    Debugger memasuki kode berdasarkan dasar per-utas. Misalnya, ini berarti bahwa jika Anda menekan F5 untuk melanjutkan eksekusi, dan aplikasi mencapai titik henti berikutnya, itu dapat memecah kode pada utas yang berbeda. Jika Anda perlu mengelola perilaku ini untuk tujuan penelusuran kesalahan, Anda dapat menambahkan titik henti tambahan, titik henti kondisional, atau menggunakan Break All. Untuk informasi selengkapnya tentang cara menggunakan titik henti kondisional, lihat Mengikuti satu utas dengan titik henti kondisional.

  3. Pilih Debug > Tumpukan Paralel Windows > untuk membuka jendela Tumpukan Paralel, lalu pilih Utas dari menu dropdown Tampilan di jendela.

    Cuplikan layar Tampilan Threads di jendela Tumpukan Paralel.

    Dalam tampilan Utas, kerangka tumpukan dan jalur panggilan utas saat ini disorot dengan warna biru. Lokasi utas saat ini diperlihatkan oleh panah kuning.

    Perhatikan label untuk tumpukan panggilan untuk Gorilla_Start adalah 2 Thread. Ketika Anda terakhir kali menekan F5, Anda memulai satu utas lagi. Untuk penyederhanaan dalam aplikasi kompleks, tumpukan panggilan yang identik dikelompokkan bersama ke dalam satu representasi visual. Ini menyederhanakan informasi yang berpotensi kompleks, terutama dalam skenario dengan banyak utas.

    Selama debugging, Anda dapat beralih penampilan kode eksternal. Untuk mengalihkan fitur, pilih atau hapus Perlihatkan Kode Eksternal. Jika Anda menampilkan kode eksternal, Anda masih dapat menggunakan panduan ini, tetapi hasil Anda mungkin berbeda dari ilustrasi.

  4. Tekan F5 lagi, dan debugger berhenti pada baris Debugger.Break() dalam metode MorningWalk.

    Jendela Parallel Stacks menunjukkan lokasi utas eksekusi saat ini dalam MorningWalk metode .

    Cuplikan layar dari tampilan Threads setelah F5.

  5. Letakkan kursor di atas metode MorningWalk untuk mendapatkan informasi tentang dua utas yang diwakili oleh tumpukan panggilan yang dikelompokkan.

    Cuplikan layar utas yang terkait dengan tumpukan panggilan.

    Utas saat ini juga muncul di daftar Utas di toolbar Debug.

    Cuplikan layar utas saat ini di toolbar Debug.

    Anda dapat menggunakan daftar Utas untuk mengalihkan konteks debugger ke utas yang berbeda. Ini tidak mengubah utas eksekusi saat ini, melainkan hanya mengubah konteks debugger.

    Atau, Anda dapat mengubah konteks debugger dengan mengeklik dua kali metode di tampilan Utas, atau dengan mengklik kanan metode di tampilan Utas dan memilih Beralih ke Frame>[ID utas].

  6. Tekan F5 lagi dan debugger berhenti pada metode MorningWalk untuk utas kedua.

    Cuplikan layar tampilan Utas setelah F5 kedua.

    Bergantung pada waktu eksekusi utas, pada titik ini Anda dapat melihat tumpukan panggilan terpisah atau dikelompokkan.

    Pada ilustrasi sebelumnya, tumpukan panggilan dari dua utas tersebut dikelompokkan secara parsial. Segmen identik dari tumpukan panggilan dikelompokkan, dan garis panah menunjuk ke segmen yang dipisahkan (yaitu, tidak identik). Bingkai tumpukan saat ini ditandai dengan penyorotan biru.

  7. Tekan F5 lagi, dan Anda akan melihat penundaan panjang terjadi dan tampilan Utas tidak menampilkan informasi tumpukan panggilan apa pun.

    Penundaan ini disebabkan oleh kebuntuan. Tidak ada yang muncul di tampilan Utas karena meskipun utas mungkin diblokir, Anda saat ini tidak dihentikan sementara di debugger.

    Note

    Di C++, Anda juga melihat kesalahan debug yang menunjukkan bahwa abort() telah dipanggil.

    Tip

    Tombol Putuskan Semua adalah cara yang baik untuk mendapatkan informasi tumpukan panggilan jika terjadi deadlock atau semua utas sedang diblokir.

  8. Di bagian atas IDE di toolbar Debug, pilih tombol Hentian Semua (ikon jeda), atau gunakan Ctrl + Alt + Break.

    Cuplikan layar tampilan Utas setelah memilih Hentikan Semua.

    Bagian atas tumpukan panggilan dalam tampilan Utas memperlihatkan yang FindBananas di-deadlock. Penunjuk eksekusi di FindBananas adalah panah hijau melengkung, menunjukkan konteks debugger saat ini tetapi juga memberi tahu kita bahwa utas saat ini tidak berjalan.

    Note

    Di C++, Anda tidak melihat informasi dan ikon "kebuntuan" yang berguna terdeteksi. Namun, Anda masih menemukan panah hijau melengkung di Jungle.FindBananas, mengisyaratkan lokasi kebuntuan.

    Di editor kode, kita menemukan panah hijau melengkung dalam lock fungsi . Dua utas diblokir pada fungsi lock dalam metode FindBananas.

    Cuplikan layar editor kode setelah memilih Hentian Semua.

    Tergantung pada urutan eksekusi utas, kebuntuan muncul dalam pernyataan lock(tree) atau lock(banana_bunch).

    Panggilan untuk lock memblokir utas dalam metode FindBananas. Satu utas menunggu kunci pada tree dilepaskan oleh utas lain, tetapi utas lainnya menunggu kunci pada banana_bunch dilepaskan dulu sebelum dapat melepaskan kunci pada tree. Ini adalah contoh kebuntuan klasik yang terjadi ketika dua utas saling menunggu.

    Jika Anda menggunakan Copilot, Anda juga bisa mendapatkan ringkasan utas yang dihasilkan AI untuk membantu mengidentifikasi potensi kebuntuan.

    Cuplikan layar deskripsi ringkasan alur Copilot.

Memperbaiki kode sampel

Untuk memperbaiki kode ini, selalu dapatkan beberapa kunci dalam urutan global yang konsisten di semua utas. Ini mencegah menunggu melingkar dan menghilangkan kebuntuan.

  1. Untuk memperbaiki kebuntuan, ganti kode di MorningWalk dengan kode berikut.

    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. Mulai ulang aplikasi.

Summary

Panduan ini menunjukkan jendela debugger Parallel Stacks . Gunakan jendela ini pada proyek nyata yang menggunakan kode multithreaded. Anda dapat memeriksa kode paralel yang ditulis dalam C++, C#, atau Visual Basic.