Aracılığıyla paylaş


.NET Framework 2.0'da Yürüyen Profil Oluşturucu Yığını: Temel Bilgiler ve Ötesi

 

Eylül 2006

David Broman
Microsoft Corporation

Aşağıdakiler cihazlar için geçerlidir:
   Microsoft .NET Framework 2.0
   Ortak Dil Çalışma Zamanı (CLR)

Özet: Profil oluşturucunuzu .NET Framework ortak dil çalışma zamanında (CLR) yönetilen yığınlarda yürüyecek şekilde nasıl programlayabileceğinizi açıklar. (14 yazdırılan sayfa)

İçindekiler

Giriş
Zaman Uyumlu ve Zaman Uyumsuz Çağrılar
Karıştırma
En İyi Davranışınızda Olun
Yetti artık
Kredinin Süresi Dolacağı Kredi
Yazar hakkında

Giriş

Bu makale, yönetilen uygulamaları incelemek için profil oluşturucu oluşturmak isteyen herkese yöneliktir. Profil oluşturucunuzu, .NET Framework ortak dil çalışma zamanında (CLR) yönetilen yığınlarda yürüyecek şekilde nasıl programlayabileceğinizi açıklayacak. Ruh halini hafif tutmaya çalışacağım, çünkü konu bazen ağır olabilir.

CLR'nin 2.0 sürümündeki profil oluşturma API'sinde, profil oluşturucunuzun profil oluşturduğunuz uygulamanın çağrı yığınında gezinmesine olanak tanıyan DoStackSnapshot adlı yeni bir yöntemi vardır. CLR'nin 1.1 sürümü, işlem içi hata ayıklama arabirimi aracılığıyla benzer işlevleri kullanıma sunar. Ancak DoStackSnapshot ile çağrı yığınını yürümek daha kolay, daha doğru ve daha kararlıdır. DoStackSnapshot yöntemi, atık toplayıcı, güvenlik sistemi, özel durum sistemi vb. tarafından kullanılan yığın yürütücüsü kullanır. Yani doğru olması gerektiğini biliyorsun .

Tam yığın izlemesine erişim, profil oluşturucunuzun kullanıcılarına ilginç bir şey olduğunda bir uygulamada neler olduğunu büyük bir şekilde anlama olanağı sağlar. Uygulamaya ve kullanıcının profiline bağlı olarak, bir kullanıcının bir nesne ayrıldığında, bir sınıf yüklendiğinde, özel durum oluşturulduğunda vb. çağrı yığını istediğini düşünebilirsiniz. Bir uygulama olayı (örneğin zamanlayıcı olayı) dışında bir şey için çağrı yığını almak bile örnekleme profil oluşturucu için ilginç olabilir. Koddaki sık erişimli noktalara bakmak, etkin noktayı içeren işlevi çağıran işlevi çağıran işlevi kimin çağırdığını gördüğünüzde daha aydınlatıcı hale gelir.

DoStackSnapshot API'siyle yığın izlemeleri almaya odaklanacağım. Yığın izlemelerini almanın bir diğer yolu da gölge yığınlar oluşturmaktır: Geçerli iş parçacığı için yönetilen çağrı yığınının bir kopyasını tutmak için FunctionEnter ve FunctionLeave'e bağlanabilirsiniz. Gölge yığın oluşturma, uygulama yürütme sırasında her zaman yığın bilgilerine ihtiyacınız varsa ve profil oluşturucunuzun kodunun yönetilen her çağrıda çalıştırılıp döndürülmesi için performans maliyetinin sakıncası yoksa yararlıdır. DoStackSnapshot yöntemi, olaylara yanıt olarak olduğu gibi yığınların biraz daha hızlı raporlanması gerekiyorsa en iyisidir. Birkaç milisaniyede bir yığın anlık görüntüleri alan bir örnekleme profili oluşturucu bile gölge yığınlar oluşturmaktan çok daha zayıftır. DoStackSnapshot profil oluşturucu örneklemesi için çok uygundur.

Vahşi Tarafta YığınLa Yürüyüş Yapın

Çağrı yığınlarını istediğiniz zaman alabilmeniz çok yararlıdır. Ama güçle birlikte sorumluluk da gelir. Profil oluşturucu kullanıcısı, yığın yürüyüşlerinin erişim ihlaline (AV) veya çalışma zamanında kilitlenmeye neden olmasını istemez. Profil yazarı olarak gücünü dikkatli kullanmalısın. DoStackSnapshot'ı nasıl kullanacağınızı ve bunu nasıl dikkatle yapacağımı konuşacağım. Gördüğünüz gibi, bu yöntemle ne kadar çok yapmak istiyorsanız, doğru olanı yapmak o kadar zor olur.

Konumuza bir göz atalım. Profil oluşturucunuz şunları çağırır (bunu Corprof.idl'deki ICorProfilerInfo2 arabiriminde bulabilirsiniz):

HRESULT DoStackSnapshot( 
  [in] ThreadID thread, 
  [in] StackSnapshotCallback *callback, 
  [in] ULONG32 infoFlags, 
  [in] void *clientData, 
  [in, size_is(contextSize), length_is(contextSize)] BYTE context[], 
  [in] ULONG32 contextSize); 

Aşağıdaki kod, CLR'nin profil oluşturucunuzda çağıran kodudur. (Bunu Corprof.idl'de de bulabilirsiniz.) Önceki örnekte yer alan geri çağırma parametresinde bu işlevi uygulamanıza bir işaretçi geçirirsiniz.

typedef HRESULT __stdcall StackSnapshotCallback( 
  FunctionID funcId, 
  UINT_PTR ip, 
  COR_PRF_FRAME_INFO frameInfo, 
  ULONG32 contextSize, 
  BYTE context[], 
  void *clientData); 

Sandviç gibi. Profil oluşturucunuz yığında gezinmek istediğinde DoStackSnapshot'ı çağırırsınız. CLR bu çağrıdan dönmeden önce , StackSnapshotCallback işlevinizi her yönetilen çerçeve için veya yığındaki yönetilmeyen çerçevelerin her çalıştırması için birkaç kez çağırır. Şekil 1'de bu sandviç gösterilmektedir.

Şekil 1. Profil oluşturma sırasında çağrıların "sandviçi"

Gösterimimde de görebileceğiniz gibi CLR, çerçeveleri yığına nasıl gönderildiklerinden ters sırada bildirir; önce yaprak çerçeve (son itilmiş), ana çerçeve son (önce itildi).

Bu işlevlerin tüm parametreleri ne anlama gelir? Henüz hepsini tartışmaya hazır değilim, ancak DoStackSnapshot'tan başlayarak bunlardan birkaçını tartışacağım. (Geri kalanına birkaç dakika içinde ulaşacağım.) infoFlags değeri Corprof.idl'deki COR_PRF_SNAPSHOT_INFO numaralandırmasından gelir ve CLR'nin size rapor verdiği çerçeveler için kayıt bağlamları verip vermeyeceğini denetlemenizi sağlar. clientData için istediğiniz herhangi bir değeri belirtebilirsiniz ve CLR bunu StackSnapshotCallback çağrınızda size geri verir.

StackSnapshotCallback'te CLR, funcId parametresini kullanarak o anda yürüdüğü çerçevenin FunctionID değerini geçirir. Geçerli çerçeve yönetilmeyen çerçevelerden oluşan bir çalıştırmaysa bu değer 0'dır. Daha sonra bahsedeceğim. funcId sıfır değilse, işlev hakkında daha fazla bilgi edinmek için funcId ve frameInfo değerlerini GetFunctionInfo2 ve GetCodeInfo2 gibi diğer yöntemlere geçirebilirsiniz. Yığın yürüyüş sırasında bu işlev bilgilerini hemen alabilir veya alternatif olarak funcId değerlerini kaydedebilir ve işlev bilgilerini daha sonra alabilirsiniz; bu da çalışan uygulama üzerindeki etkinizi azaltır. İşlev bilgilerini daha sonra alırsanız, frameInfo değerinin yalnızca size veren geri çağırma içinde geçerli olduğunu unutmayın. FuncId değerlerini daha sonra kullanmak üzere kaydetmek sorun olmasa da, frameInfo'u daha sonra kullanmak üzere kaydetmeyin.

StackSnapshotCallback'ten döndüğünüzde genellikle S_OK döndürür ve CLR yığında yürümeye devam eder. İsterseniz , yığın adımını durduran S_FALSE döndürebilirsiniz. DoStackSnapshot çağrınız CORPROF_E_STACKSNAPSHOT_ABORTED döndürür.

Zaman Uyumlu ve Zaman Uyumsuz Çağrılar

DoStackSnapshot'ı zaman uyumlu ve zaman uyumsuz olarak iki şekilde çağırabilirsiniz. Zaman uyumlu bir çağrı en kolay doğru olanıdır. CLR, profil oluşturucunuzun ICorProfilerCallback(2) yöntemlerinden birini çağırdığında zaman uyumlu bir çağrı yaparsınız ve yanıt olarak geçerli iş parçacığının yığınında gezinmek için DoStackSnapshot'ı çağırırsınız. ObjectAllocated gibi ilginç bir bildirim noktasında yığının nasıl göründüğünü görmek istediğinizde bu yararlı olur. Zaman uyumlu bir çağrı gerçekleştirmek için ICorProfilerCallback(2) yönteminizin içinden DoStackSnapshot'ı çağırarak size bahsetmediğim parametreler için sıfır veya null değerini geçirirsiniz.

Farklı bir iş parçacığının yığınında yürüdüğünde veya bir iş parçacığını bir yığın yürüyüş gerçekleştirmek için zorla böldüğünde (kendisinde veya başka bir iş parçacığında) zaman uyumsuz yığın kılavuzu oluşur. bir iş parçacığını kesmek, iş parçacığının yönerge işaretçisini ele geçirerek rastgele zamanlarda kendi kodunuzu yürütmeye zorlamayı içerir. Burada listelenemeyecek kadar çok nedenden dolayı bu çok tehlikeli. Lütfen, bunu yapma. Zaman uyumsuz yığın adımlarının açıklamasını DoStackSnapshot'ın ele geçirilmeyen kullanımlarıyla kısıtlayarak ayrı bir hedef iş parçacığında yürüyeceğim. Hedef iş parçacığı yığın kılavuzu başladığında rastgele bir noktada yürütüldüğünden bunu "zaman uyumsuz" olarak adlandırıyorum. Bu teknik genellikle örnekleme profil oluşturucuları tarafından kullanılır.

Başka birinin üzerinde yürümek

Şimdi çapraz iş parçacığını (zaman uyumsuz) parçalayalım, yığın biraz yürüyelim. İki iş parçacığınız var: geçerli iş parçacığı ve hedef iş parçacığı. Geçerli iş parçacığı DoStackSnapshot'i yürüten iş parçacığıdır. Hedef iş parçacığı, yığını DoStackSnapshot tarafından ilerletilen iş parçacığıdır. Hedef iş parçacığını, iş parçacığı kimliğini iş parçacığı parametresinde DoStackSnapshot'a geçirerek belirtirsiniz. Bundan sonra olacak şey, kalbi zayıf olanlar için değil. Hedef iş parçacığının, yığınında gezinmek istediğinizde rastgele kod yürüttüğünu unutmayın. Bu nedenle CLR hedef iş parçacığını askıya alır ve yüründüğü süre boyunca askıya alınır. Bu güvenli bir şekilde yapılabilir mi?

Sorduğuna sevindim. Bu gerçekten tehlikeli ve daha sonra bunu güvenli bir şekilde nasıl yapacağım hakkında konuşacağım. Ama önce karma mod yığınlarına geçeceğim.

Karıştırma

Yönetilen bir uygulamanın tüm zamanını yönetilen kodda geçirmesi olası değildir. PInvoke çağrıları ve COM birlikte çalışma, yönetilen kodun yönetilmeyen koda çağrı yapmalarına ve bazen de temsilcilerle yeniden aramalarına olanak sağlar. Yönetilen kod, JIT derlemesi yapmak, özel durumları işlemek, çöp toplamayı gerçekleştirmek vb. için doğrudan yönetilmeyen çalışma zamanına (CLR) çağrı yapar. Bu nedenle, yığın yürüyüş yaparken büyük olasılıkla karma mod yığınla karşılaşırsınız; bazı çerçeveler yönetilen işlevler, diğerleri ise yönetilmeyen işlevlerdir.

Büyü, şimdiden!

Devam etmeden önce kısa bir ara. Modern bilgisayarlarımızdaki yığınların daha küçük adreslere büyüdüğünü (yani "gönderme") herkes bilir. Ancak bu adresleri zihnimizde veya beyaz tahtalarda görselleştirdiğimizde, bunları dikey olarak sıralama konusunda aynı fikirde değiliz. Bazılarımız yığının büyüdüğünü hayal ediyor (üstte küçük adresler); bazıları küçüldüğünü görür (alttaki küçük adresler). Ekibimizde de bu konuya bölünmüş durumdayız. Daha önce kullandığım herhangi bir hata ayıklayıcının yanında yer almayı seçiyorum; çağrı yığını izlemeleri ve bellek dökümleri bana küçük adreslerin büyük adreslerin "üstünde" olduğunu söylüyor. Böylece yığınlar büyür; ana alttadır, yaprak callee en üsttedir. Katılmıyorsanız, makalenin bu bölümünü geçmek için biraz zihinsel yeniden düzenleme yapmanız gerekir.

Garson, Yığınımda Delikler Var

Artık aynı dili konuşmaya devam ettiğimize göre karma mod yığınına göz atalım. Şekil 2'de örnek bir karma mod yığını gösterilmektedir.

Şekil 2. Yönetilen ve yönetilmeyen çerçeveleri olan bir yığın

Biraz geri adım atarak DoStackSnapshot'ın neden mevcut olduğunu anlamanız faydalı olabilir. Yığında yönetilen çerçeveleri izlemenize yardımcı olmak için oradadır. Yönetilen çerçeveleri kendiniz gezdirmeye çalışırsanız, yönetilen kodda kullanılan bazı saçma çağırma kuralları nedeniyle özellikle 32 bit sistemlerde güvenilir olmayan sonuçlar elde edersiniz. CLR bu çağrı kurallarını anlar ve DoStackSnapshot bu nedenle bunların kodunu çözmenize yardımcı olabilir. Ancak doStackSnapshot , yönetilmeyen çerçeveler de dahil olmak üzere yığının tamamını yürüyebilmek istiyorsanız tam bir çözüm değildir.

İşte burada bir seçeneğiniz vardır:

1. Seçenek: Hiçbir şey yapma ve "yönetilmeyen delikler" içeren yığınları kullanıcılarınıza bildirme veya ...

Seçenek 2: Bu delikleri doldurmak için kendi yönetilmeyen yığın yürütücünüzü yazın.

DoStackSnapshot yönetilmeyen karelerden oluşan bir blokla karşı karşıya geldiğinde, daha önce de belirttiğim gibi funcId değeri 0 olarak ayarlanmış StackSnapshotCallback işlevinizi çağırır. Seçenek 1'i kullanıyorsanız funcId değeri 0 olduğunda geri aramanızda hiçbir şey yapmanız yeterli değildir. CLR sizi bir sonraki yönetilen çerçeve için yeniden çağırır ve bu noktada uyanabilirsiniz.

Yönetilmeyen blok birden fazla yönetilmeyen çerçeveden oluşuyorsa CLR yine de StackSnapshotCallback'i yalnızca bir kez çağırır. CLR'nin yönetilmeyen bloğun kodunu çözmek için hiçbir çaba göstermediğini unutmayın; bloğu bir sonraki yönetilen çerçeveye atlamasına yardımcı olan özel insider bilgilerine sahiptir ve bu şekilde ilerler. CLR' nin yönetilmeyen bloğun içinde ne olduğunu bilmesi gerekmez. Bu sizin çözmeniz için, dolayısıyla Seçenek 2.

İlk Adım Bir Doozy

Hangi seçeneği seçerseniz seçin, yönetilmeyen delikleri doldurmak tek zor kısım değildir. Yürüyüşe yeni başlamak zor olabilir. Yukarıdaki yığına göz atın. En üstte yönetilmeyen kod var. Bazen şanslı olursunuz ve yönetilmeyen kod COM veya PInvoke kodu olur. Öyleyse, CLR nasıl atlanacağını bilecek kadar akıllıdır ve ilk yönetilen çerçevede (örnekte D) yürüyüşe başlar. Ancak, mümkün olduğunca eksiksiz bir yığın bildirmek için en üstteki yönetilmeyen blokta gezinmek isteyebilirsiniz.

En üstteki blokta yürümek istemeseniz bile yine de zorunlu olabilirsiniz ; şanslı değilseniz , yönetilmeyen kod COM veya PInvoke kodu değil, JIT derleme veya çöp toplama kodu gibi CLR'nin kendisinde yardımcı koddur. Böyle bir durumda CLR, yardımınız olmadan D çerçevesini bulamaz. Bu nedenle DoStackSnapshot'a gönderilmeyen bir çağrı CORPROF_E_STACKSNAPSHOT_UNMANAGED_CTX veya CORPROF_E_STACKSNAPSHOT_UNSAFE hatasına neden olur. (Bu arada, corerror.h.'yi ziyaret etmek gerçekten faydalı olacaktır.)

"Korumasız" sözcüğünü kullandığıma dikkat edin. DoStackSnapshot, context ve contextSize parametrelerini kullanarak bir tohum bağlamı alır. "Bağlam" sözcüğü birçok anlama sahip aşırı yüklenmiştir. Bu durumda kayıt bağlamından bahsediyorum. Mimariye bağımlı windows üst bilgilerini (örneğin, nti386.h) incelediğinizde CONTEXT adlı bir yapı bulursunuz. CPU yazmaçları için değerler içerir ve CPU'nun belirli bir zamanda durumunu temsil eder. İşte bahsettiğim bağlam bu.

Bağlam parametresi için null geçirirseniz yığın kılavuzunun korumasızdır ve CLR en üstten başlar. Ancak, bağlam parametresi için null olmayan bir değer geçirirseniz ve yığında daha düşük bir noktada CPU durumunu gösterirseniz (örneğin, D çerçevesine işaret eder), CLR bağlamınızla çekirdeklenmiş bir yığın kılavuzu gerçekleştirir. Yığının gerçek üst kısmını yoksayar ve işaret ettiğiniz her yerde başlar.

Tamam, pek doğru değil. DoStackSnapshot'a geçirdiğiniz bağlam, doğrudan yönergeden daha fazla ipucudur. CLR ilk yönetilen çerçeveyi bulabileceğinden eminse (en üstteki yönetilmeyen blok PInvoke veya COM kodu olduğundan), bunu yapar ve tohumunuzu yoksayar. Bunu kişisel olarak algılama. CLR, sağlayabileceğiniz en doğru yığın yürüyüşlerini sağlayarak size yardımcı olmaya çalışıyor. Çekirdeğiniz yalnızca en üstteki yönetilmeyen bloğun CLR'nin kendisinde yardımcı kod olması durumunda yararlıdır, çünkü bunu atlamamıza yardımcı olacak bir bilgimiz yoktur. Bu nedenle, tohumunuz yalnızca CLR tek başına yürüyüşe nereden başlayacağını belirleyemediğinde kullanılır.

Tohumu bize nasıl sağlayabileceğinizi merak edebilirsiniz. Hedef iş parçacığı henüz askıya alınmamışsa, D çerçevesini bulmak ve böylece tohum bağlamınızı hesaplamak için hedef iş parçacığının yığınında yürüyemezsiniz. Yine de DoStackSnapshot'ıçağırmadan önce yönetilmeyen yürüyüşünüzü yaparak ve doStackSnapshot hedef iş parçacığını sizin için askıya almadan önce tohum bağlamınızı hesaplamanızı söylüyorum. Hedef iş parçacığının siz ve CLR tarafından askıya alınması gerekiyor mu? Aslında, evet.

Bence bu balenin koreografisi zamanı geldi. Ancak çok derine inmeden önce, yığın yürüyüşünün nasıl ve nasıl dağıtıldığı sorununun yalnızca zaman uyumsuz yürüyüşler için geçerli olduğunu unutmayın. Zaman uyumlu bir yürüyüş yapıyorsanız DoStackSnapshot , yardımınız olmadan her zaman en üstteki yönetilen kareye giden yolu bulabilir; tohuma gerek yoktur.

Şimdi Hep Birlikte

Yönetilmeyen delikleri doldururken zaman uyumsuz, çapraz iş parçacığı, tohumlu yığın yürüyüş yapan gerçekten maceracı profil oluşturucu için, yığın yürüyüş şöyle görünür. Burada gösterilen yığının Şekil 2'de gördüğünüz yığınla aynı olduğunu varsayalım, yalnızca biraz ayrılmıştır.

Yığın İçeriği Profil oluşturucu ve CLR eylemleri

1. Hedef iş parçacığını askıya alırsınız. (Hedef iş parçacığının askıya alma sayısı şimdi 1'dir.)

2. Hedef iş parçacığının geçerli yazmaç bağlamını alırsınız.

3. Yazmaç bağlamının yönetilmeyen kodu işaret edip etmediğini belirlersiniz; yani ICorProfilerInfo2::GetFunctionFromIP'yi çağırırsınız ve 0 functionID değerini geri alıp almayabileceğinizi denetlersiniz.

4. Bu örnekte yazmaç bağlamı yönetilmeyen kodu işaret ettiğinden, en üstteki yönetilen çerçeveyi (İşlev D) bulana kadar yönetilmeyen bir yığın kılavuzu gerçekleştirirsiniz.

5. DoStackSnapshot'ı tohum bağlamınızla çağırırsınız ve CLR hedef iş parçacığını yeniden askıya alır. (Askıya alma sayısı şimdi 2'dir.) Sandviç başlıyor.
a. CLR, D için FunctionID ile StackSnapshotCallback işlevinizi çağırır.
b. CLR, FunctionID değeri 0'a eşit olan StackSnapshotCallback işlevinizi çağırır. Bu blokta kendi başına yürümelisin. İlk yönetilen çerçeveye ulaştığınızda durdurabilirsiniz. Alternatif olarak, bir sonraki geri aramadan bir süre sonraya kadar yönetilmeyen yürüyüşünüzü aldatabilir ve geciktirebilirsiniz, çünkü bir sonraki geri arama size tam olarak bir sonraki yönetilen çerçevenin nerede başladığını ve böylece yönetilmeyen yürüyüşünüzün nerede bitmesi gerektiğini söyler.
c. CLR, C için FunctionID ile StackSnapshotCallback işlevinizi çağırır.
d. CLR, B için FunctionID değeriyle StackSnapshotCallback işlevinizi çağırır.
e. CLR, FunctionID değeri 0'a eşit olan StackSnapshotCallback işlevinizi çağırır. Yine, bu blokta kendi başınıza yürümeniz gerekir.
f. CLR, A için FunctionID ile StackSnapshotCallback işlevinizi çağırır.
g. CLR, Main için FunctionID ile StackSnapshotCallback işlevinizi çağırır.

h. DoStackSnapshot Win32 ResumeThread() API'sini çağırarak hedef iş parçacığını "sürdürür", bu da iş parçacığının askıya alma sayısını azaltır (askıya alma sayısı şimdi 1'dir) ve döndürür. Sandviç tamamlandı.
6. Hedef iş parçacığını sürdürürseniz. Askıya alma sayısı şu anda 0 olduğundan iş parçacığı fiziksel olarak devam eder.

En İyi Davranışınızda Olun

Tamam, ciddi bir uyarı olmadan bu çok fazla güç. En gelişmiş durumda, zamanlayıcı kesmelerine yanıt veriyor ve uygulama iş parçacıklarını yığınlarında gezinmek için rastgele askıya alıyorsunuz. Yikes!

İyi olmak zordur ve başta belirgin olmayan kurallar içerir. O zaman içeri dalalım.

Kötü Tohum

Kolay bir kuralla başlayalım: hatalı bir tohum kullanmayın. DoStackSnapshot'ı çağırdığınızda profil oluşturucunuz geçersiz (null olmayan) bir çekirdek sağlarsa, CLR size kötü sonuçlar verir. Yığını işaret ettiğiniz yere bakar ve yığındaki değerlerin neyi temsil edeceğine ilişkin varsayımlarda bulunur. Bu, CLR'nin yığındaki adresler olduğu varsayılan başvuruyu kaldırmasına neden olur. Hatalı bir tohum verüldüğünde, CLR değerleri bellekte bilinmeyen bir yere başvurur. CLR, tamamen ikinci şans AV'yi önlemek için her şeyi yapar ve bu da profil oluşturduğunuz işlemi yok eder. Ama tohumunuzu doğru yapmak için gerçekten çaba göstermelisiniz.

Askıya Alma Sorunları

İş parçacıklarını askıya alma işleminin diğer yönleri, birden çok kural gerektirecek kadar karmaşıktır. Çapraz iş parçacığıyla yürüyüş yapmaya karar verdiğinizde, CLR'den sizin adınıza iş parçacıklarını askıya almasını istemeye en azından karar verdiniz. Dahası, yığının en üstündeki yönetilmeyen blokta yürümek istiyorsanız, clr'nin bilgeliğini çağırmadan iş parçacıklarını şu anda iyi bir fikir olup olmadığına karar verdiniz.

Bilgisayar bilimi dersleri aldıysanız muhtemelen "yemek filozofları" sorununu hatırlarsınız. Bir grup filozof bir masada oturuyor, her biri sağda bir çatal ve solda bir çatal var. Soruna göre, her birlerinin yemek için iki çatala ihtiyacı var. Her filozof sağ çatalını alır, ancak o zaman kimse sol çatalını kaldıramıyor çünkü her filozof, filozofu solunda bekleyen gerekli çatalı koymak için bekliyor. Ve filozoflar dairesel bir masada oturuyorlarsa, bir bekleme döngüsü ve bir sürü boş mideniz vardır. Hepsinin açlıktan ölmelerinin nedeni, basit bir kilitlenme önleme kuralını bozmalarıdır: birden çok kilide ihtiyacınız varsa, her zaman aynı sırayla alın. Bu kurala uyulması A'nın B'de, B'nin C'de ve C'nin A'da beklediği döngüden kaçınacak.

Bir uygulamanın kuralı izlediğini ve kilitleri her zaman aynı sırada aldığını varsayalım. Şimdi bir bileşen ortaya çıkar (örneğin profil oluşturucunuz) ve iş parçacıklarını rastgele askıya alma işlemini başlatır. Karmaşıklık önemli ölçüde artmıştır. Askıya alma işleminin şimdi askıya alma tarafından tutulan bir kilidi alması gerekiyorsa ne olur? Ya da askıya alma işleminin, askıya alma tarafından tutulan bir kilidi bekleyen başka bir iş parçacığı tarafından tutulan bir iş parçacığı tarafından tutulması gerekiyorsa ne olur? Askıya alma, iş parçacığı bağımlılığı grafımıza döngüler ekleyebilen yeni bir kenar ekler. Bazı belirli sorunlara göz atalım.

Sorun 1: Askıya alan, askıya alma için gereken veya askıya alma işleminin bağımlı olduğu iş parçacıkları tarafından gereken kilitlere sahiptir.

Sorun 1a: Kilitler CLR kilitleridir.

Tahmin edebileceğiniz gibi, CLR çok fazla iş parçacığı eşitlemesi gerçekleştirir ve bu nedenle dahili olarak kullanılan birkaç kilidi vardır. DoStackSnapshot'ı çağırdığınızda CLR, hedef iş parçacığının yığın adım adım ilerletmek için geçerli iş parçacığının (DoStackSnapshot'ı çağıran iş parçacığı) ihtiyaç duyduğu bir CLR kilidine sahip olduğunu algılar. Bu durum ortaya çıktığında CLR askıya almayı reddeder ve DoStackSnapshot hemen hata CORPROF_E_STACKSNAPSHOT_UNSAFE döndürür. Bu noktada, DoStackSnapshot çağrısından önce yazışmayı kendiniz askıya aldıysanız, yazışmayı kendiniz sürdürür ve bir sorundan kaçınmış olursunuz.

Sorun 1b: Kilitler kendi profil oluşturucunuzun kilitleridir.

Bu sorun daha çok anlamlı bir sorundur. Burada ve orada yapmanız gereken kendi iş parçacığı eşitlemeniz olabilir. Bir uygulama iş parçacığının (İş Parçacığı A) bir profil oluşturucu geri çağırması ile karşılaştığını ve profil oluşturucunun kilitlerinden birini alan profil oluşturucu kodunuzun bir kısmını çalıştırdığını düşünün. Ardından, İş Parçacığı B'nin İş Parçacığı A'ya yürümesi gerekir; bu da İş Parçacığı B'nin İş Parçacığı A'yı askıya alması anlamına gelir. A İş Parçacığı askıya alınırken, İş Parçacığı B'nin A İş Parçacığı'nın sahip olabileceği profil oluşturucunun kendi kilitlerinden herhangi birini almaya çalışmaması gerektiğini unutmayın. Örneğin, İş Parçacığı B, yığın yürüyüşü sırasında StackSnapshotCallback yürütür, bu nedenle bu geri çağırma sırasında İş Parçacığı A'ya ait olabilecek hiçbir kilit almamalısınız.

Sorun 2: Hedef iş parçacığını askıya alırken, hedef iş parçacığı sizi askıya almaya çalışır.

"Bu olamaz!" diyebilirsiniz. İster inanın ister inanmayın, şu şekilde olabilir:

  • Uygulamanız çok işlemcili bir kutuda çalışır ve
  • İş Parçacığı A bir işlemcide, B İş Parçacığı ise başka bir işlemcide çalışır ve
  • İş Parçacığı A, İş Parçacığı B İş Parçacığını askıya almaya çalışırken B İş Parçacığını askıya almaya çalışır.

Bu durumda, her iki askıya alma da kazanır ve her iki iş parçacığının da askıya alınması mümkündür. Çünkü her bir iş parçacığı diğerinin onu uyandırmasını beklediğinden, sonsuza kadar askıya alınmış durumda kalırlar.

DoStackSnapshot'ıçağırmadan önce iş parçacıklarının birbirini askıya almasını algılamak için CLR'ye güvenemediğinizden, bu sorun Sorun 1'den daha rahatsız edicidir. Ve askıya alma işlemini yaptıktan sonra, artık çok geç!

Hedef iş parçacığı neden profil oluşturucuyu askıya almaya çalışıyor? Varsayımsal, kötü yazılmış bir profil oluşturucuda, yığın yürüme kodu ve askıya alma kodu rastgele zamanlarda herhangi bir sayıda iş parçacığı tarafından yürütülebilir. İş Parçacığı A'nın, İş Parçacığı B'nin İş Parçacığı A'da gezinmeye çalıştığı aynı zamanda İş Parçacığı B'yi de izlemeye çalıştığını düşünün. Her ikisi de profil oluşturucunun yığın yürüme yordamının SuspendThread bölümünü yürütürken birbirlerini aynı anda askıya almaya çalışır. Hem kazanır hem de profili oluşturulan uygulama kilitlenir. Buradaki kural açıktır; profil oluşturucunuzun aynı anda iki iş parçacığında yığın yürüme kodu (ve dolayısıyla askıya alma kodu) yürütmesine izin verme!

Hedef iş parçacığının yürüme ipliğinizi askıya almaya çalışmasının daha az belirgin bir nedeni CLR'nin iç çalışmalarından kaynaklanır. CLR, atık toplama gibi görevlere yardımcı olmak için uygulama iş parçacıklarını askıya alır. Yürütücünüz, çöp toplayıcı iş parçacığının yürütücünüzü askıya almaya çalıştığı sırada çöp toplama işlemini gerçekleştiren iş parçacığını yürümeye (ve dolayısıyla askıya almaya) çalışırsa, işlemler kilitlenir.

Ancak sorundan kaçınmak kolaydır. CLR yalnızca işini yapmak için askıya alması gereken iş parçacıklarını askıya alır. Yığın yürüyüşünüzün iki iş parçacığı olduğunu düşünün. W İş Parçacığı geçerli iş parçacığıdır (adımları gerçekleştiren iş parçacığı). İş Parçacığı T, hedef iş parçacığıdır (yığınına yürünmüş olan iş parçacığı). İş Parçacığı W hiçbir zaman yönetilen kodu yürütmediği ve bu nedenle CLR çöp toplamaya tabi olmadığı sürece, CLR hiçbir zaman İş Parçacığı W'yi askıya almayı denemez. Bu, profil oluşturucunuzun İş Parçacığı W'nin İş Parçacığı T'yi askıya almasının güvenli olduğu anlamına gelir.

Bir örnekleme profili oluşturucu yazıyorsanız, bunların tümünü güvence altına almak oldukça doğaldır. Genellikle zamanlayıcı kesmelerine yanıt veren ve diğer iş parçacıklarının yığınlarını gösteren kendi oluşturduğunuz ayrı bir iş parçacığına sahip olursunuz. Bunu örnekleyici iş parçacığınız olarak adlandırabilirsiniz. Örnekleyici iş parçacığını kendiniz oluşturduğunuz ve yürütecekleri üzerinde denetiminiz olduğundan (ve bu nedenle yönetilen kodu hiçbir zaman yürütmediğinden), CLR'nin bunu askıya almak için bir nedeni olmaz. Profil oluşturucunuzun tüm yığın yürüyüşlerini yapmak için kendi örnekleme iş parçacığını oluşturacak şekilde tasarlanması, daha önce açıklanan "kötü yazılmış profil oluşturucu" sorununu da önler. Örnekleyici iş parçacığı, diğer iş parçacıklarını yürümeye veya askıya almaya çalışan profil oluşturucunuzun tek iş parçacığıdır, bu nedenle profil oluşturucunuz hiçbir zaman örnekleyici iş parçacığını doğrudan askıya almaya çalışmaz.

Bu bizim ilk önemsiz kuralımızdır, bu nedenle vurgulamam için tekrarlamama izin verin:

Kural 1: Yalnızca hiçbir zaman yönetilen kodu çalıştırmamış bir iş parçacığı başka bir iş parçacığını askıya almalıdır.

Kimse Bir Cesedi Yürümeyi Sevmez

Çapraz iş parçacığı yığın yürüyüşü gerçekleştiriyorsanız, hedef iş parçacığınızın adım boyunca canlı kaldığından emin olmanız gerekir. Hedef iş parçacığını DoStackSnapshot çağrısına parametre olarak geçirmeniz, buna örtük olarak herhangi bir yaşam süresi başvurusu eklediğiniz anlamına gelmez. Uygulama, iş parçacığının istediğiniz zaman gitmesini sağlayabilir. İş parçacığında gezinmeye çalışırken böyle bir durum ortaya çıkarsa, erişim ihlaline kolayca neden olursunuz.

Neyse ki CLR, ICorProfilerCallback(2) arabirimiyle tanımlanan aptly named ThreadDestroyed geri çağırmasını kullanarak bir iş parçacığı yok edilmek üzere olduğunda profil oluşturuculara bildirimde bulunur. ThreadDestroyed'ı uygulamak ve iş parçacığının yürümesi bitene kadar beklemesini sağlamak sizin sorumluluğunuzdadır. Bu, bir sonraki kuralımız olarak nitelendirilecek kadar ilginçtir:

Kural 2: ThreadDestroyed geri çağırmasını geçersiz kılın ve uygulamanızın yok edilecek iş parçacığı yığınını yürümeyi bitirene kadar beklemesini sağlayın.

Aşağıdaki Kural 2, o iş parçacığının yığınını yürümeyi bitirene kadar CLR'nin iş parçacığını yok etmesine engel olur.

Çöp Toplama Bir Döngü Yapmanıza Yardımcı Olur

Bu noktada işler biraz kafa karıştırıcı olabilir. Sonraki kuralın metniyle başlayalım ve buradan deşifre edelim:

Kural 3: Çöp toplamayı tetikleyebilen bir profil oluşturucu çağrısı sırasında kilit tutmayın.

Daha önce, sahip olan iş parçacığı askıya alınmışsa ve iş parçacığının aynı kilide ihtiyaç duyan başka bir iş parçacığı tarafından yürünebilmesi durumunda profil oluşturucunuzun kendi kilidi varsa bir tane tutmasının kötü bir fikir olduğunu belirtmiştim. Kural 3 daha ince bir sorundan kaçınmanıza yardımcı olur. Burada, sahip olan iş parçacığı çöp toplamayı tetikleyebilecek bir ICorProfilerInfo(2) yöntemini çağırmak üzereyse kendi kilitlerinizi tutmamanız gerektiğini söylüyorum.

Birkaç örnek yardımcı olabilir. İlk örnekte, İş Parçacığı B'nin çöp toplamayı yaptığını varsayalım. Sıra şu şekildedir:

  1. İş Parçacığı A alır ve artık profil oluşturucu kilitlerinizden birine sahip olur.
  2. B İş Parçacığı profil oluşturucunun GarbageCollectionStarted geri çağırmasını çağırır.
  3. 1. Adım'dan profil oluşturucu kilidindeki İş Parçacığı B blokları.
  4. A İş Parçacığı GetClassFromTokenAndTypeArgs işlevini yürütür .
  5. GetClassFromTokenAndTypeArgs çağrısı bir çöp toplamayı tetiklemeye çalışır, ancak bir çöp toplama işleminin zaten devam ettiğini algılar.
  6. İş Parçacığı A blokları, şu anda devam eden çöp toplamanın (İş Parçacığı B) tamamlanmasını bekliyor. Ancak, profil oluşturucu kilidiniz nedeniyle İş Parçacığı B, İş Parçacığı A'yı bekliyor.

Şekil 3'te bu örnekteki senaryo gösterilmektedir:

Şekil 3. Profil oluşturucu ve çöp toplayıcı arasında kilitlenme

İkinci örnek biraz farklı bir senaryodur. Sıra şu şekildedir:

  1. A yazışması profil oluşturucu kilitlerinizden birini alır ve bu kilitlere sahip olur.
  2. İş Parçacığı B, profil oluşturucunun ModuleLoadStarted geri çağırmasını çağırır.
  3. 1. Adım'dan profil oluşturucu kilidindeki İş Parçacığı B blokları.
  4. İş Parçacığı A , GetClassFromTokenAndTypeArgs işlevini yürütür .
  5. GetClassFromTokenAndTypeArgs çağrısı bir çöp toplamayı tetikler.
  6. A İş Parçacığı (şimdi çöp toplamayı yapıyor), İş Parçacığı B'nin toplanmaya hazır olmasını bekler. Ancak profil oluşturucu kilidiniz nedeniyle İş Parçacığı B, İş Parçacığı A'nın gelmesini bekliyor.
  7. Şekil 4'de ikinci örnek gösterilmektedir.

Şekil 4. Profil oluşturucu ile bekleyen çöp toplama arasında kilitlenme

Deliliği sindirdin mi? Sorunun en önemli noktası, çöp toplamanın kendi eşitleme mekanizmalarına sahip olmasıdır. İlk örnekteki sonuç, aynı anda yalnızca bir çöp toplama işlemi olabileceğinden oluşur. Bu açıkça kabul edilir, çünkü çöp toplama işlemleri genellikle çok sık gerçekleşmez ve siz stresli koşullar altında çalışmadığınız sürece bir başkası için beklemeniz gerekir. Yine de, yeterince uzun bir profil oluşturursanız bu senaryo gerçekleşir ve buna hazırlıklı olmanız gerekir.

İkinci örnekteki sonuç, çöp toplama işlemini gerçekleştiren iş parçacığının diğer uygulama iş parçacıklarının toplamaya hazır olmasını beklemesi gerektiğinden oluşur. Sorun, kendi kilitlerinizden birini karışıma eklediğinizde ortaya çıkar ve böylece bir döngü oluşturur. Her iki durumda da Kural 3, İş Parçacığı A'nın profil oluşturucu kilitlerinden birine sahip olmasına ve ardından GetClassFromTokenAndTypeArgs çağrısına izin vererek bozulur. (Aslında, bir çöp toplamayı tetikleyebilecek herhangi bir yöntemi çağırmak işlemi mahkum etmek için yeterlidir.)

Muhtemelen şimdiye kadar birkaç sorunuz vardır.

S. Hangi ICorProfilerInfo(2) yöntemlerinin çöp toplamayı tetikleyebileceğini nasıl anlarsınız?

A. Bunu MSDN'de veya en azından blogumda veya Jonathan Keljo'nun blogunda belgeleyeceğiz.

S. Bunun yığınla yürümeyle ne ilgisi var? DoStackSnapshot'tan bahsedilmiyor.

A. Doğru. DoStackSnapshot, çöp toplamayı tetikleyen ICorProfilerInfo(2) yöntemlerinden biri bile değildir. Burada Kural 3'ü tartışmamın nedeni, bu maceracı programcıların kendi profil oluşturucu kilitlerini uygulama olasılığı en yüksek olan rastgele örneklerden gelen yığınları zaman uyumsuz olarak yürümeleri ve bu yüzden bu tuzağa düşmeye eğilimli olmalarıdır. Aslında, Kural 2 temelde profil oluşturucunuza eşitleme eklemenizi söyler. Bir örnekleme profili oluşturucunun, paylaşılan veri yapılarını rastgele zamanlarda okumak ve yazmak için koordine etmek için başka eşitleme mekanizmaları da olması muhtemeldir. Elbette, DoStackSnapshot'a hiç dokunmayan bir profil oluşturucunun bu sorunla karşılaşması mümkündür.

Yetti artık

Önemli noktaların kısa bir özetiyle bitireceğim. Hatırlamanız gereken önemli noktalar şunlardır:

  • Zaman uyumlu yığın yürüyüşleri, bir profil oluşturucu geri çağırmasına yanıt olarak geçerli iş parçacığını yürümeyi içerir. Bunlar için tohumlama, askıya alma veya özel kurallar gerekmez.
  • Yığının üst kısmı yönetilmeyen kodsa ve PInvoke veya COM çağrısının parçası değilse zaman uyumsuz yürüyüşler için bir tohum gerekir. Hedef iş parçacığını doğrudan askıya alarak ve en üstteki yönetilen çerçeveyi bulana kadar kendiniz yürüyerek bir tohum sağlarsınız. Bu durumda bir tohum sağlamazsanız DoStackSnapshot bir hata kodu döndürebilir veya yığının en üstündeki bazı kareleri atlayabilir.
  • İş parçacıklarını askıya almanız gerekiyorsa, yalnızca yönetilen kodu hiç çalıştırmamış bir iş parçacığının başka bir iş parçacığını askıya alması gerektiğini unutmayın.
  • Zaman uyumsuz yürüyüşler gerçekleştirirken, iş parçacığının yığın kılavuzu tamamlanana kadar CLR'nin bir iş parçacığını yok etmelerini engellemek için her zaman ThreadDestroyed geri çağırmasını geçersiz kılın.
  • Profil oluşturucunuz çöp toplamayı tetikleyebilen bir CLR işlevine çağrı yaparken kilit tutmayın.

Profil oluşturma API'si hakkında daha fazla bilgi için MSDN Web sitesindeki Profil Oluşturma (Yönetilmeyen) bölümüne bakın.

Kredinin Süresi Dolacağı Kredi

ClR Profil Oluşturma API'sinin geri kalanına teşekkür etmek istiyorum, çünkü bu kuralları yazmak gerçekten bir ekip çalışmasıydı. Bu içeriğin büyük bir kısmının daha erken bir enkarnasyonunun sağlanmasını sağlayan Sean Selitrennikoff'a teşekkür ederiz.

 

Yazar hakkında

David, sınırlı bilgi ve olgunluğu göz önünde bulundurulduğunda düşündüğünüzden daha uzun süredir Microsoft'ta geliştirici olarak çalışıyor. Artık kodu iade etmesine izin verilmiyor olsa da, yeni değişken adları için fikirler sunar. David, Kont Chocula'nın hevesli bir hayranıdır ve kendi arabasına sahip.