Aracılığıyla paylaş


İş Parçacıkları görünümünü kullanarak kilitlenme hatası ayıklama

Bu öğreticide, çok iş parçacıklı bir uygulamada hata ayıklamak için Paralel Yığınlar pencerelerininİş Parçacıkları görünümünün nasıl kullanılacağı gösterilmektedir. Bu pencere, çok iş parçacıklı kodun çalışma zamanı davranışını anlamanıza ve doğrulamanıza yardımcı olur.

C#, C++ ve Visual Basic için İş Parçacıkları Görünümü desteklenir. C# ve C++ için örnek kod sağlanır, ancak bazı kod başvuruları ve çizimleri yalnızca C# örnek kodu için geçerlidir.

İş Parçacıkları görünümü şunları oluşturmanıza yardımcı olur:

  • Birden çok iş parçacığı için çağrı yığını görselleştirmelerini görüntüleyerek, yalnızca geçerli iş parçacığının çağrı yığınını gösteren Çağrı Yığını penceresinden daha kapsamlı bir uygulama durumu elde edin.

  • Engellenen veya kilitlenmiş iş parçacıkları gibi sorunları tanımlamaya yardımcı olun.

Çok iş parçacıklı çağrı yığınları

Çağrı yığınının aynı bölümleri, karmaşık uygulamalar için görselleştirmeyi basitleştirmek üzere birlikte gruplandırılır.

Aşağıdaki kavramsal animasyon, gruplandırma işleminin çağrı yığınlarına nasıl uygulandığını gösterir. Çağrı yığınının yalnızca aynı kesimleri gruplandırılır. İş parçacıklarını tanımlamak için gruplandırılmış çağrı yığınının üzerine gelin.

Çağrı yığınlarının gruplandırma çizimi.

Örnek koda genel bakış (C#, C++)

Bu kılavuzdaki örnek kod, bir gorilin hayatında bir gün simülasyonu sağlayan bir uygulamaya yöneliktir. Alıştırmanın amacı, çok iş parçacıklı bir uygulamada hata ayıklamak için Paralel Yığınlar penceresinin İş Parçacıkları görünümünün nasıl kullanılacağını anlamaktır.

Örnek, iki iş parçacığı birbirini beklerken oluşan bir kilitlenme durumu örneğini içerir.

Çağrı yığınını sezgisel hale getirmek için örnek uygulama aşağıdaki sıralı adımları gerçekleştirir:

  1. Gorilleri temsil eden bir nesne oluşturur.
  2. Goril uyanır.
  3. Goril sabah yürüyüşüne çıkıyor.
  4. Goril ormanda muz bulur.
  5. Goril yiyor.
  6. Goril maymun işine girer.

Örnek projeyi oluşturma

Projeyi oluşturmak için:

  1. Visual Studio'yu açın ve yeni bir proje oluşturun.

    Başlangıç penceresi açık değilse DosyaBaşlangıç Penceresiseçin.

    Başlangıç penceresinde Yeni proje'yi seçin.

    Yeni proje oluştur penceresinde, arama kutusuna konsol girin veya yazın. Ardından, Dil listesinden C# veya C++ öğesini seçin ve ardından Platform listesinden Windows'u seçin.

    Dil ve platform filtrelerini uyguladıktan sonra, seçtiğiniz dil için Konsol Uygulaması'nı ve ardından İleri'yi seçin.

    Note

    Doğru şablonu görmüyorsanız Visual Studio Yükleyicisi'ni açan Araçlar>Araç ve Özellik Al... seçeneğine gidin. .NET masaüstü geliştirme iş yükünü seçin ve ardından Değiştir'iseçin.

    Yeni projenizi yapılandırın penceresinde, proje adı kutusuna bir ad yazın veya varsayılan adı kullanın. Ardından İleri'yi seçin.

    .NET projesi için önerilen hedef çerçeveyi veya .NET 8'i ve ardından Oluştur'u seçin.

    Yeni bir konsol projesi görünür. Proje oluşturulduktan sonra bir kaynak dosya görüntülenir.

  2. Projede .cs (veya .cpp) kod dosyasını açın. Boş bir kod dosyası oluşturmak için içeriğini silin.

  3. Seçtiğiniz dil için aşağıdaki kodu boş kod dosyasına yapıştırın.

     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. Dosya menüsünde Tümünü Kaydet'i seçin.

  5. Derle menüsünde Çözüm Derle'yi seçin.

Paralel Yığınlar penceresinin İş Parçacıkları Görünümünü kullanın

Hata ayıklamayı başlatmak için:

  1. Hata Ayıkla menüsünde Hata Ayıklamayı Başlat 'ı (veya F5) seçin ve ilkin Debugger.Break() isabetini bekleyin.

    Note

    C++'ta hata ayıklayıcı __debug_break() içinde duraklatılır. Bu makaledeki kod başvurularının ve çizimlerinin geri kalanı C# sürümüne yöneliktir, ancak aynı hata ayıklama ilkeleri C++ için de geçerlidir.

  2. F5 tuşuna bir kez bastığınızda hata ayıklayıcı aynı Debugger.Break() satırda yeniden duraklatılır.

    Bu, ikinci iş parçacığında gerçekleşen ikinci çağrı sırasında Gorilla_Start duraklatılır.

    Tip

    Hata ayıklayıcısı iş parçacığı başına koda girer. Örneğin, yürütmeye devam etmek için F5 tuşuna basarsanız ve uygulama bir sonraki kesme noktasına ulaştığında, farklı bir iş parçacığındaki koda geçebilir. Hata ayıklama amacıyla bu davranışı yönetmeniz gerekiyorsa, ek kesme noktaları, koşullu kesme noktaları ekleyebilir veya Tümünü Kes'i kullanabilirsiniz. Koşullu kesme noktalarını kullanma hakkında daha fazla bilgi için bkz. Koşullu kesme noktalarıyla tek bir iş parçacığını izleme.

  3. Paralel Yığınlar penceresini açmak için Windows > Paralel Yığınlarında Hata Ayıkla'yı > seçin ve ardından penceredeki Görünüm açılan listesinden İş Parçacıkları'nı seçin.

    Paralel Yığınlar penceresindeki İş Parçacıkları görünümünün ekran görüntüsü.

    İş Parçacıkları görünümünde, geçerli iş parçacığının yığın çerçevesi ve çağrı yolu mavi renkle vurgulanır. İş parçacığının şu anki konumu sarı okla gösterilmektedir.

    Çağrı yığınının Gorilla_Start etiketinin 2 İş Parçacığı olduğuna dikkat edin. F5'e en son bastığınızda başka bir iş parçacığı başlattınız. Karmaşık uygulamalarda basitleştirme için, aynı çağrı yığınları tek bir görsel gösterimde birlikte gruplandırılır. Bu, özellikle birçok iş parçacığına sahip senaryolarda karmaşık olabilecek bilgileri basitleştirir.

    Hata ayıklama sırasında dış kodun görüntülenip görüntülenmeyeceğini değiştirebilirsiniz. Özelliği değiştirmek için Dış Kodu Göster'i seçin veya temizleyin. Dış kod gösterirseniz, bu kılavuzu kullanmaya devam edebilirsiniz, ancak sonuçlarınız çizimlerden farklı olabilir.

  4. F5 tuşuna yeniden basın ve hata ayıklayıcı Debugger.Break() yöntemindeki MorningWalk satırında durur.

    Paralel Yığınlar penceresinde, MorningWalk yönteminde mevcut yürütülen iş parçacığının konumu gösterilir.

    Konular görünümünün F5'ten sonra ekran görüntüsü.

  5. Toplanmış çağrı yığını tarafından temsil edilen iki iş parçacığı hakkında bilgi almak için MorningWalk yöntem üzerine gel.

    Çağrı yığınıyla ilişkili iş parçacıklarının ekran görüntüsü.

    Geçerli iş parçacığı, Debug araç çubuğundaki İş Parçacığı listesinde de görünür.

    Debug araç çubuğundaki geçerli iş parçacığının ekran görüntüsü.

    Hata ayıklayıcısı bağlamını farklı bir iş parçacığına değiştirmek için İş Parçacığı listesini kullanabilirsiniz. Bu işlem geçerli yürütülen iş parçacığını değiştirmez, yalnızca hata ayıklayıcı bağlamını değiştirir.

    Alternatif olarak, İş Parçacıkları görünümünde bir yönteme çift tıklayarak veya İş Parçacıkları görünümünde bir yönteme sağ tıklayıp Çerçeveye> Geç[iş parçacığı kimliği] seçeneğini belirleyerek hata ayıklayıcısı bağlamını değiştirebilirsiniz.

  6. F5 tuşuna yeniden bastığınızda hata ayıklayıcı, ikinci iş parçacığının MorningWalk yönteminde duraklatılır.

    İkinci F5'in ardından İş Parçacıkları görünümünün ekran görüntüsü.

    İş parçacığı yürütme zamanlamasına bağlı olarak, bu noktada ayrı veya gruplandırılmış çağrı yığınları görürsünüz.

    Yukarıdaki çizimde, iki iş parçacığının çağrı yığınları kısmen gruplandırılmıştır. Çağrı yığınlarının özdeş kesimleri gruplandırılır ve ok çizgileri ayrılmış olan kesimleri (yani aynı değil) işaret eder. Geçerli yığın çerçevesi mavi vurgulamayla gösterilir.

  7. F5 tuşuna yeniden bastığınızda uzun bir gecikme yaşandığını ve İş Parçacıkları görünümünün herhangi bir çağrı yığını bilgisi göstermediğini görürsünüz.

    Gecikme, kilitlenmeden kaynaklanır. İş parçacıkları engellenmiş olsa bile, hataları ayıklama işleminin durakta durmadığınız için İş Parçacıkları görünümünde hiçbir şey görünmez.

    Note

    C++'ta, abort() çağrıldığına dair bir debug hatası da görürsünüz.

    Tip

    Kilitlenme oluşursa veya tüm iş parçacıkları şu anda engellenirse Tümünü Durdur düğmesi çağrı yığını bilgilerini almak için etkili bir yöntemdir.

  8. Hata Ayıklama araç çubuğundaki IDE'nin üst kısmında Tümünü Kes düğmesini seçin (duraklatma simgesi) veya Ctrl + Alt + Break tuşlarını kullanın.

    Tümünü Sonlandır'ı seçtikten sonra İş Parçacıkları görünümünün ekran görüntüsü.

    İş Parçacıkları görünümündeki çağrı yığını üst kısmında FindBananas'in kilitlendiği gösteriliyor. içindeki FindBananas yürütme işaretçisi, geçerli hata ayıklayıcısı bağlamını gösteren kıvrılmış yeşil bir ok olsa da, iş parçacıklarının şu anda çalışmadığını da bildirir.

    Note

    C++'de faydalı "kilitlenme algılandı" bilgilerini ve simgelerini göremezsiniz. Ancak, yine de kilitlenmenin konumunu işaret eden kıvrılmış yeşil oku Jungle.FindBananas içinde bulursunuz.

    Kod düzenleyicisinde lock işlevinde eğik yeşil oku buluyoruz. İki iş parçacığı, lock yöntemindeki FindBananas işlevinde engelleniyor.

    Tümünü Kes'i seçtikten sonra kod düzenleyicisinin ekran görüntüsü.

    İş parçacığı yürütme sırasına bağlı olarak, kilitlenme ya lock(tree) ya da lock(banana_bunch) deyiminde görünür.

    lock çağrısı, FindBananas yöntemindeki iş parçacıklarını engeller. Bir iş parçacığı tree üzerindeki kilidin diğer iş parçacığı tarafından serbest bırakılmasını bekliyor, ancak diğer iş parçacığı banana_bunch üzerindeki kilidi bırakabilmek için tree üzerindeki kilidin serbest bırakılmasını bekliyor. Bu, iki iş parçacığı birbirini beklerken oluşan klasik bir kilitlenme örneğidir.

    Copilot kullanıyorsanız olası kilitlenmeleri tanımlamaya yardımcı olmak için yapay zeka tarafından oluşturulan iş parçacığı özetlerini de alabilirsiniz.

    Copilot iş parçacığı özet açıklamalarının ekran görüntüsü.

Örnek kodu düzeltme

Bu kodu düzeltmek için, her zaman tüm iş parçacıklarında tutarlı ve evrensel bir sırayla birden çok kilidi temin edin. Bu, döngüsel beklemeleri önler ve kilitlenmeleri ortadan kaldırır.

  1. Kilitlenmeyi düzeltmek için içindeki MorningWalk kodu aşağıdaki kodla değiştirin.

    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. Uygulamayı yeniden başlatın.

Summary

Bu kılavuzda Parallel Stacks hata ayıklayıcısı penceresi gösterilmiştir. Çok iş parçacıklı kod kullanan gerçek projelerde bu pencereyi kullanın. C++, C# veya Visual Basic ile yazılmış paralel kodu inceleyebilirsiniz.