Kode ini hang dalam mode rilis tetapi berfungsi dengan baik dalam mode debug


110

Saya menemukan ini dan ingin tahu alasan perilaku ini dalam mode debug & rilis.

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
apa sebenarnya perbedaan perilakunya?
Mong Zhu

4
Jika itu java saya akan berasumsi bahwa compiler tidak melihat update ke variabel 'compile'. Menambahkan 'volatile' ke deklarasi variabel akan memperbaikinya (dan menjadikannya bidang statis).
Sebastian


4
Catatan: inilah mengapa pengembangan multithread memiliki hal-hal seperti mutex dan operasi atom. Saat Anda mulai multithread, Anda perlu mempertimbangkan serangkaian masalah memori tambahan yang luar biasa yang tidak jelas sebelumnya. Alat sinkronisasi threading seperti mutex akan menyelesaikan masalah ini.
Cort Ammon

5
@DavidSchwartz: Tentu saja itu diizinkan . Dan compiler, runtime, dan CPU diizinkan untuk membuat hasil kode itu berbeda dari yang Anda harapkan. Secara khusus, C # tidak diizinkan untuk membuat akses bool nonatomik , tetapi diizinkan untuk memindahkan pembacaan non-volatile ke belakang pada waktunya. Ganda, sebaliknya, tidak memiliki batasan atomisitas; pembacaan dan penulisan ganda pada dua utas berbeda tanpa sinkronisasi diizinkan untuk robek.
Eric Lippert

Jawaban:


149

Saya kira pengoptimal tertipu oleh kurangnya kata kunci 'volatile' pada isCompletevariabel.

Tentu saja, Anda tidak dapat menambahkannya, karena ini adalah variabel lokal. Dan tentu saja, karena ini adalah variabel lokal, ini tidak diperlukan sama sekali, karena penduduk lokal disimpan di tumpukan dan secara alami selalu "segar".

Namun , setelah dikompilasi, itu bukan lagi variabel lokal . Karena diakses dalam delegasi anonim, kode tersebut dipisahkan, dan diterjemahkan ke dalam kelas pembantu dan bidang anggota, seperti:

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;
    }
}

Saya dapat membayangkan sekarang bahwa kompilator / pengoptimal JIT di lingkungan multithread, saat memproses TheHelperkelas, sebenarnya dapat menyimpan nilai falsedalam cache di beberapa bingkai register atau tumpukan pada awalBody() metode, dan tidak pernah menyegarkannya hingga metode berakhir. Itu karena TIDAK ADA JAMINAN bahwa utas & metode TIDAK akan berakhir sebelum "= true" dijalankan, jadi jika tidak ada jaminan, mengapa tidak menyimpannya dan mendapatkan peningkatan kinerja dari membaca objek heap sekali daripada membacanya di setiap pengulangan.

Inilah mengapa kata kunci itu volatileada.

Untuk kelas penolong ini menjadi benar sedikit lebih baik 1) di lingkungan multi-threaded, ia harus memiliki:

    public volatile bool isComplete = false;

tetapi, tentu saja, karena ini adalah kode yang dibuat secara otomatis, Anda tidak dapat menambahkannya. Pendekatan yang lebih baik adalah dengan menambahkan beberapa hal lock()di sekitar baca dan tulis isCompleted, atau menggunakan beberapa sinkronisasi siap pakai atau utilitas threading / tasking daripada mencoba melakukannya bare-metal (yang tidak akan terjadi. bare-metal, karena C # di CLR dengan GC, JIT dan (..)).

Perbedaan dalam mode debug terjadi mungkin karena dalam mode debug banyak pengoptimalan yang dikecualikan, jadi Anda dapat men-debug kode yang Anda lihat di layar. Oleh karena while (!isComplete)itu, tidak dioptimalkan sehingga Anda dapat menyetel breakpoint di sana, dan oleh karena isCompleteitu tidak disimpan dalam cache secara agresif dalam register atau tumpukan saat metode dimulai dan dibaca dari objek di heap pada setiap iterasi pengulangan.

BTW. Itu hanya tebakan saya tentang itu. Saya bahkan tidak mencoba untuk mengkompilasinya.

BTW. Sepertinya bukan bug; itu lebih seperti efek samping yang sangat tidak jelas. Juga, jika saya benar tentang itu, maka itu mungkin kekurangan bahasa - C # harus memungkinkan untuk menempatkan kata kunci 'volatile' pada variabel lokal yang ditangkap dan dipromosikan ke bidang anggota di closures.

1) lihat di bawah untuk komentar dari Eric Lippert sekitar volatiledan / atau artikel ini sangat menarik yang menunjukkan tingkat kompleksitas yang terlibat dalam memastikan bahwa kode mengandalkan volatileadalah aman ..uh, baik ..uh, katakanlah OK.


2
@EricLippert: whoa, terima kasih banyak untuk konfirmasi secepat itu! Bagaimana menurut Anda, adakah kemungkinan bahwa dalam beberapa versi mendatang kita bisa mendapatkan volatileopsi pada variabel lokal capture-to-closure? Saya membayangkan ini bisa agak sulit untuk diproses oleh kompiler ..
quetzalcoatl

7
@quetzalcoatl: Saya tidak akan berharap fitur itu akan ditambahkan dalam waktu dekat. Ini adalah jenis pengkodean yang ingin Anda hindari , dan bukan membuatnya lebih mudah . Dan selain itu, membuat barang tidak stabil tidak selalu menyelesaikan setiap masalah. Berikut adalah contoh di mana semuanya tidak stabil dan programnya masih salah; dapatkah kamu menemukan bugnya? blog.coverity.com/2014/03/26/reordering-optimizations
Eric Lippert

3
Dimengerti. Saya berhenti mencoba memahami pengoptimalan multithreading ... sungguh gila betapa rumitnya itu.
peralihan

10
@Pikoh: Sekali lagi, berpikirlah seperti pengoptimal. Anda memiliki variabel yang bertambah tetapi tidak pernah dibaca. Variabel yang tidak pernah dibaca dapat dihapus seluruhnya.
Eric Lippert

4
@EricLippert sekarang pikiran saya membuat klik. Utas ini sangat informatif, terima kasih banyak, sungguh.
Pikoh

82

The jawaban Quetzalcoatl benar. Untuk menjelaskan lebih lanjut:

Compiler C # dan jitter CLR diizinkan untuk membuat banyak pengoptimalan yang mengasumsikan bahwa thread saat ini adalah satu-satunya thread yang berjalan. Jika pengoptimalan tersebut membuat program salah di dunia di mana utas saat ini bukan satu-satunya utas yang berjalan, itu adalah masalah Anda . Anda dibutuhkan untuk menulis program multithread yang memberitahu compiler dan jitter hal-hal gila multithread yang Anda lakukan.

Dalam kasus khusus ini, jitter diizinkan - tetapi tidak diperlukan - untuk mengamati bahwa variabel tidak diubah oleh badan loop dan karena itu menyimpulkan bahwa - karena dengan asumsi ini adalah satu-satunya thread yang berjalan - variabel tidak akan pernah berubah. Jika tidak pernah berubah maka variabel perlu diperiksa kebenarannya sekali , tidak setiap kali melalui loop. Dan inilah yang sebenarnya terjadi.

Bagaimana mengatasi ini? Jangan menulis program multithread . Multithreading sangat sulit dilakukan dengan benar, bahkan bagi para ahli. Jika harus, gunakan mekanisme tingkat tertinggi untuk mencapai tujuan Anda . Solusinya di sini bukanlah membuat variabel mudah menguap. Solusinya di sini adalah menulis tugas yang dapat dibatalkan dan menggunakan mekanisme pembatalan Perpustakaan Paralel Tugas . Biarkan TPL khawatir tentang mendapatkan logika threading dengan benar dan pembatalan dengan benar mengirim seluruh utas.


1
Komentar tidak untuk diskusi panjang; percakapan ini telah dipindahkan ke obrolan .
Hantu Madara

14

Saya melekat pada proses yang sedang berjalan dan menemukan (jika saya tidak membuat kesalahan, saya tidak terlalu terlatih dengan ini) bahwa Threadmetode tersebut diterjemahkan menjadi ini:

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

eax(yang berisi nilai isComplete) dimuat pertama kali dan tidak pernah dimuat ulang.


8

Bukan jawaban yang sebenarnya, tetapi untuk menjelaskan lebih banyak tentang masalah ini:

Masalahnya sepertinya kapan i dideklarasikan di dalam tubuh lambda dan itu hanya dibaca di ekspresi penugasan. Jika tidak, kode berfungsi dengan baik dalam mode rilis:

  1. i dideklarasikan di luar tubuh lambda:

    int i = 0; // Declared outside the lambda body
    
    var t = new Thread(() =>
    {
        while (!isComplete) { i += 0; }
    }); // Completes in release mode
  2. i tidak dibaca dalam ekspresi tugas:

    var t = new Thread(() =>
    {
        int i = 0;
        while (!isComplete) { i = 0; }
    }); // Completes in release mode
  3. i juga dibaca di tempat lain:

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

Taruhan saya adalah beberapa kompiler atau optimasi JIT tentang imengacaukan banyak hal. Seseorang yang lebih pintar dari saya mungkin bisa menjelaskan lebih banyak tentang masalah ini.

Meskipun demikian, saya tidak akan terlalu khawatir tentang itu, karena saya gagal melihat di mana kode serupa benar-benar melayani tujuan apa pun.


1
lihat jawaban saya, saya cukup yakin ini semua tentang kata kunci 'volatile' yang tidak dapat ditambahkan ke variabel lokal (yang sebenarnya kemudian dipromosikan ke bidang anggota dalam penutupan) ..
quetzalcoatl
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.