2017-01-13 6 views
109

デバッグ&リリースモードでこの現象が発生した理由を知りたいと思います。このコードはリリースモードでハングアップしますが、デバッグモードで正常に動作します

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

動作の違いはなんですか? –

+2

これは厄介です。 'i'がラムダの外側で宣言されていれば、コードはデバッグとリリースモードで正常に動作しますが、ラムダ本体の中で宣言されていれば、リリースモードでコードがハングします。一部の最適化が間違っていますか? – InBetween

+4

もしそれがjavaなら、コンパイラは 'コンパイル'変数への更新を見ないと思うでしょう。変数宣言に 'volatile'を追加するとそれが修正され、静的フィールドになります。 – Sebastian

答えて

8
本当に

ない答えが、問題にいくつかのより多くの光を当てるために:

問題がiが、それは割り当てにのみを読んだラムダ本体内で宣言されたときのようです表現。ラムダ本体の外側で宣言

  1. i

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

    int i = 0; // Declared outside the lambda body 
    
    var t = new Thread(() => 
    { 
        while (!isComplete) { i += 0; } 
    }); // Completes in release mode 
    
  3. iが代入式に読まないそうでなければ、コードがリリースモードでも動作します他の場所でも読み取られます:

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

私の賭けはiに関するいくつかのコンパイラやJITの最適化は、物事をめちゃくちゃにされています。おそらく、私より賢い誰かが、この問題についてより多くの光を当てることができるだろう。

それにもかかわらず、似たようなコードが実際にどこの目的に役立つのかわからないので、あまり心配しません。

+1

私の答えを見て、私はローカル変数(実際には後で閉鎖のメンバフィールドに昇格される)に追加することはできませんが、 'volatile'キーワードに関するすべてだと確信しています。 – quetzalcoatl

14

私はThread方法はこれに翻訳されていることを(私はミスをしなかった場合、私は非常にこれを実施されないよ)、実行中のプロセスに接続し、見つかった:

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

eax(含まれていますisCompleteの値)が初めてロードされ、更新されませんでした。

149

isComplete変数に 'volatile'キーワードがないため、オプティマイザが騙されているようです。

もちろん、ローカル変数であるため追加できません。もちろんローカル変数であるので、ローカル変数はスタックに保存されており、当然常に「新鮮」なので、まったく必要ではありません。

コンパイル後、ローカル変数ではなく、です。それは匿名デリゲートでアクセスされているので、コードが分割され、そしてそれはヘルパークラスやメンバフィールドに翻訳され、のようなもの:

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

私は、マルチスレッド環境でのJITコンパイラ/オプティマイザことを今想像することができますTheHelperクラスを処理する場合、実際にキャッシュの値falseをいくつかのレジスタまたはスタックフレームのBody()メソッドの開始時に取得し、メソッドが終了するまで更新しないでください。これは、スレッド&のメソッドが "= true"が実行される前に終了しないという保証がないため、保証がない場合は、キャッシュしてヒープオブジェクトを読み込む代わりに一度読み込むパフォーマンスを向上させるすべての反復で。

これは、正確にはキーワードvolatileが存在する理由です。なるように、このヘルパー・クラスについては

正しい 小さな小さなビットマルチスレッド環境での1)より良い、それが持っている必要があります:それは自動生成なので、当然のことながら、

public volatile bool isComplete = false; 

をが、コードを追加することはできません。より良いアプローチは、lock()を読み取りと書き込みのあちこちにisCompletedに追加するか、またはベアメタルを実行するのではなく、すぐに使用できる他の同期またはスレッド/タスクユーティリティを使用することです(になりません)。 GC、JIT、(..)を使ったCLRのC#ですから、ベアメタルです。

デバッグモードの違いは、おそらくデバッグモードでは多くの最適化が除外されているため、画面に表示されているコードをデバッグできるからです。したがって、while (!isComplete)は最適化されていないので、ブレークポイントを設定することができます。したがって、isCompleteは、メソッドの開始時にレジスタまたはスタックに積極的にキャッシュされず、すべてのループ反復でヒープ上のオブジェクトから読み込まれます。

BTW。それは私の推測です。私はそれをコンパイルしようともしなかった。

BTW。それはバグではないようです。それは非常にあいまいな副作用のようなものです。また、私が正しいとすれば、それは言語の欠陥かもしれません.C#は、クロージャーのメンバーフィールドにキャプチャされ、プロモートされるローカル変数に 'volatile'キーワードを配置できるようにする必要があります。

1)volatileエリックリペットからのコメントは以下を参照および/またはvolatileに頼ってそのコードを確保することに伴う複雑さのレベルを示す、この非常に興味深い記事が 安全 ..uh、 良い です。あなたはOKと言うことにしよう。

+66

この回答は正しいです。 –

+2

@EricLippert:whoa、とても早くそのことを確認してくれてありがとう!将来のバージョンでは、キャプチャクロージャのローカル変数に 'volatile'オプションを付ける可能性はありますか?私はコンパイラで処理するのが少し難しいかもしれないと思っています。 – quetzalcoatl

+0

良いキャッチ!今問題は、なぜ 'i'(' i = 0'の代わりに 'i = 0;')に読み込みを取り除くと、問題がなくなるのでしょうか? @EricLippert – InBetween

83

answer of quetzalcoatlは正しいです。 C#コンパイラとCLRジッタは、現在のスレッドが実行中の唯一のスレッドであると仮定して非常に多くの最適化を行うことができます。これらの最適化によって、現在のスレッドだけがを実行している唯一のスレッドではない世界でプログラムが正しくない場合は、となります。あなたはが必要ですコンパイラとジッタにあなたがやっている狂ったマルチスレッドのものを伝えるマルチスレッドプログラムを書くには

この特定のケースでは、ループ本体によって変数が変更されていないことを観測することができますが、必ずしも必要ではありません。仮定することによって、これが実行中の唯一のスレッドなので、変数変更されることはありません。変数が決して変更されない場合、変数は真の場合でチェックする必要があります。そして、これは実際に起こっていることです。

これを解決するにはどうすればよいですか? マルチスレッドプログラムを書き込まないでください。専門家にとっても、マルチスレッド化は非常に困難です。必要があれば最高レベルのメカニズムを使用して目標を達成してください。ここでの解決策は、変数を揮発性にすることではありません。ここでの解決策は、にキャンセル可能なタスクを書き込んで、タスク並列ライブラリのキャンセルメカニズムを使用することです。 TPLがスレッディングロジックを正しく取得し、キャンセルがスレッド間で正しく送信されるように心配してください。

+1

コメントは議論の延長ではありません。この会話は[チャットに移動]されています(http://chat.stackoverflow.com/rooms/133276/discussion-on-answer-by-eric-lippert-this-code-hangs-in-release-mode-but-作品f)。 –

関連する問題