Bu kod yayın modunda takılıyor, ancak hata ayıklama modunda sorunsuz çalışıyor


110

Bununla karşılaştım ve hata ayıklama ve bırakma modunda bu davranışın nedenini bilmek istiyorum.

public static void Main(string[] args)
{            
   bool isComplete = false;

   var t = new Thread(() =>
   {
       int i = 0;

        while (!isComplete) i += 0;
   });

   t.Start();

   Thread.Sleep(500);
   isComplete = true;
   t.Join();
   Console.WriteLine("complete!");
}

25
davranıştaki fark tam olarak nedir?
Mong Zhu

4
Java olsaydı, derleyicinin 'derleme' değişkenindeki güncellemeleri görmediğini varsayardım. Değişken bildirimine 'uçucu' eklemek bunu düzeltir (ve onu statik bir alan haline getirir).
Sebastian


4
Not: Bu nedenle, çok iş parçacıklı geliştirme, muteksler ve atomik işlemler gibi şeylere sahiptir. Çok iş parçacıklı çalışmaya başladığınızda, daha önce aşikar olmayan dikkat çekici bir dizi ek bellek sorununu göz önünde bulundurmanız gerekir. Muteksler gibi eşleme senkronizasyon araçları bunu çözebilirdi.
Cort Ammon

5
@DavidSchwartz: Buna kesinlikle izin verilir . Ve derleyicinin, çalışma zamanının ve CPU'nun bu kodun sonucunu beklediğinizden farklı kılmasına izin verilir . Özellikle, C #, bool erişim yapmalarına izin verilmez nonatomic , ancak uçucu olmayan zamanda geriye doğru okur taşımak için izin verilir. Çiftler, aksine, atomiklik üzerinde böyle bir kısıtlamaya sahip değildir; Senkronizasyon olmadan iki farklı iş parçacığı üzerinde çift okuma ve yazma yırtılmasına izin verilir.
Eric Lippert

Yanıtlar:


149

Sanırım optimizer, isCompletedeğişkendeki 'uçucu' anahtar kelimenin olmaması nedeniyle kandırılıyor .

Elbette ekleyemezsiniz çünkü bu yerel bir değişken. Ve tabii ki, yerel bir değişken olduğu için buna hiç ihtiyaç duyulmamalıdır, çünkü yerliler yığın halinde tutulur ve doğal olarak her zaman "taze" dir.

Ancak , derledikten sonra artık yerel bir değişken değildir . Anonim bir temsilciden erişildiğinden, kod bölünür ve aşağıdaki gibi bir yardımcı sınıfa ve üye alanına çevrilir:

public static void Main(string[] args)
{
    TheHelper hlp = new TheHelper();

    var t = new Thread(hlp.Body);

    t.Start();

    Thread.Sleep(500);
    hlp.isComplete = true;
    t.Join();
    Console.WriteLine("complete!");
}

private class TheHelper
{
    public bool isComplete = false;

    public void Body()
    {
        int i = 0;

        while (!isComplete) i += 0;
    }
}

Artık çok iş parçacıklı bir ortamdaki JIT derleyicisinin / optimize edicisinin, TheHelpersınıfı işlerken , aslında yöntemin başlangıcında bir yazmaç veya yığın çerçevesindeki değeri önbelleğe alabileceğini ve yöntem bitene kadar asla yenilemeyeceğini hayal edebiliyorum . Bunun nedeni, iş parçacığı ve yöntemin "= true" çalıştırılmadan SONA ERMEYECEĞİNİN HİÇBİR GARANTİSİ olmamasıdır, bu yüzden garanti yoksa, neden önbelleğe almayasınız ve yığın nesnesini her seferinde okumak yerine bir kez okuyarak performans artışı elde etmeyiniz. yineleme.falseBody()

Anahtar kelimenin volatilevar olmasının nedeni budur .

Bu yardımcı sınıf olabilmesi için doğru bir minik daha iyi bit 1) çok kanallı ortamlarda, bu olmalıdır:

    public volatile bool isComplete = false;

ancak elbette, otomatik olarak oluşturulmuş kod olduğu için ekleyemezsiniz. Daha iyi bir yaklaşım, lock()okuma ve yazma işlemlerinin etrafına bazı metinler eklemek isCompletedveya bunu çıplak metal yapmaya çalışmak yerine kullanıma hazır diğer bazı senkronizasyon veya iş parçacığı / görevlendirme yardımcı programlarını kullanmak olacaktır (çıplak metal olmayacaktır , CLR'de GC, JIT ve (..) ile C # olduğundan).

Hata ayıklama modundaki fark büyük olasılıkla hata ayıklama modunda birçok optimizasyonun hariç tutulması nedeniyle oluşur, böylece ekranda gördüğünüz kodda hata ayıklayabilirsiniz. Bu nedenle while (!isComplete), orada bir kesme noktası ayarlayabileceğiniz için optimize edilmemiştir ve bu nedenle isComplete, yöntemin başlangıcında bir yazmaçta veya yığında agresif bir şekilde önbelleğe alınmaz ve her döngü yinelemesinde öbek üzerindeki nesneden okunur.

BTW. Bu sadece benim tahminlerim. Derlemeye bile çalışmadım.

BTW. Bir hata gibi görünmüyor; daha çok belirsiz bir yan etkiye benziyor. Ayrıca, bu konuda haklıysam, bu bir dil eksikliği olabilir - C #, yakalanan ve kapanışlarda üye alanlara yükseltilen yerel değişkenlere 'geçici' anahtar kelime yerleştirmeye izin vermelidir.

1) yaklaşık Eric Lippert bir yorumlar için aşağıya bakın volatileve / veya güvenerek bu kodu sağlamada karmaşayı seviyelerini gösteren bu çok ilginç makale volatileolan güvenli ..uh, iyi ..uh, en Tamam diyelim.


2
@EricLippert: whoa, bunu bu kadar çabuk onayladığın için çok teşekkür ederim! Nasıl düşünüyorsunuz, gelecekteki bazı sürümlerde, volatileyerel değişkenleri yakalama seçeneği elde etme şansımız var mı? Derleyici tarafından işlenmesinin biraz zor olabileceğini hayal ediyorum ..
quetzalcoatl

7
@quetzalcoatl: Bu özelliğin yakın zamanda ekleneceğine güvenmem. Bu, cesaretini kırmak isteyeceğiniz ve kolaylaştırmak istemeyeceğiniz türden bir kodlamadır . Ayrıca, bir şeyleri uçucu hale getirmek her sorunu mutlaka çözmez. İşte her şeyin uçucu olduğu ve programın hala yanlış olduğu bir örnek; hatayı bulabilir misin? blog.coverity.com/2014/03/26/reordering-optimizations
Eric Lippert

3
Anladım. Çoklu okuma optimizasyonlarını anlamaya çalışmaktan vazgeçiyorum ... ne kadar karmaşık olduğu çılgınca.
2017,

10
@Pikoh: Yine bir optimize edici gibi düşünün. Arttırılmış ama asla okunmayan bir değişkeniniz var. Asla okunmayan bir değişken tamamen silinebilir.
Eric Lippert

4
@EricLippert şimdi aklım bir tıklama yaptı. Bu konu çok bilgilendirici oldu, çok teşekkür ederim, gerçekten.
Pikoh

82

Quetzalcoatl'ın cevap doğrudur. Daha fazla ışık tutmak için:

C # derleyicisinin ve CLR jitterinin, mevcut iş parçacığının çalışan tek iş parçacığı olduğunu varsayan pek çok optimizasyon yapmasına izin verilir. Bu optimizasyonlar, mevcut iş parçacığının çalışan tek iş parçacığı olmadığı bir dünyada programı yanlış yapıyorsa, bu sizin sorununuzdur . Sen edilir gerekli yapıyorsun parçacıklı deli ne şeyler derleyici ve stres anlatmak çok iş parçacıklı programlar yazmak için.

Bu özel durumda, değişkenin döngü gövdesi tarafından değişmediğini gözlemlemesi ve dolayısıyla - varsayım gereği bu çalışan tek iş parçacığı olduğu için - değişkenin asla değişmeyeceği sonucuna varması için seğirmeye izin verilir - ancak gerekli değildir. Hiç değişmezse, değişkenin doğruluk açısından döngü boyunca her seferinde değil, bir kez kontrol edilmesi gerekir . Ve aslında olan budur.

Bunu nasıl çözebilirim? Çok iş parçacıklı programlar yazmayın . Çoklu iş parçacığını doğru kullanmak, uzmanlar için bile inanılmaz derecede zordur. Gerekirse , hedefinize ulaşmak için en üst düzey mekanizmaları kullanın . Buradaki çözüm, değişkeni uçucu hale getirmek değildir. Buradaki çözüm, iptal edilebilir bir görev yazmak ve Görev Paralel Kitaplığı iptal mekanizmasını kullanmaktır . TPL'nin iş parçacığı mantığını doğru bir şekilde alma konusunda endişelenmesine izin verin ve iptal işlemi iş parçacıkları arasında düzgün bir şekilde gönderin.


1
Yorumlar uzun tartışmalar için değildir; bu konuşma sohbete taşındı .
Madara's Ghost

14

Çalışan sürece ekledim ve buldum (hata yapmadıysam, bu konuda çok pratik yapmadım) Threadyöntemin şuna çevrildiğini:

debug051:02DE04EB loc_2DE04EB:                            
debug051:02DE04EB test    eax, eax
debug051:02DE04ED jz      short loc_2DE04EB
debug051:02DE04EF pop     ebp
debug051:02DE04F0 retn

eax(değerini içeren isComplete) ilk kez yüklenir ve asla yenilenmez.


8

Gerçekten bir cevap değil, ancak konuya biraz daha ışık tutmak için:

Sorun ne zaman gibi görünüyor ilamda vücudun içinde ilan edilir ve o oluyor sadece atama ifadesinde okuyun. Aksi takdirde kod, yayın modunda iyi çalışır:

  1. i lambda gövdesinin dışında beyan edildi:

    int i = 0; // Declared outside the lambda body
    
    var t = new Thread(() =>
    {
        while (!isComplete) { i += 0; }
    }); // Completes in release mode
  2. i atama ifadesinde okunmaz:

    var t = new Thread(() =>
    {
        int i = 0;
        while (!isComplete) { i = 0; }
    }); // Completes in release mode
  3. i ayrıca başka bir yerde okunur:

    var t = new Thread(() =>
    {
        int i = 0;
        while (!isComplete) { Console.WriteLine(i); i += 0; }
    }); // Completes in release mode

Bahse girerim bazı derleyici veya JIT optimizasyonu i, işleri karıştırmaktır. Benden daha zeki biri muhtemelen konuya daha fazla ışık tutacaktır.

Yine de, bu konuda çok fazla endişelenmem çünkü benzer kodun gerçekte herhangi bir amaca hizmet edeceği yeri göremiyorum.


1
cevabımı görün, eminim ki yerel bir değişkene eklenemeyecek 'uçucu' anahtar kelimeyle ilgili (aslında daha sonra kapanışta bir üye alanına yükseltilir) ..
quetzalcoatl
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.