このコードはリリースモードではハングしますが、デバッグモードでは正常に機能します


110

私はこれに遭遇し、デバッグとリリースモードでこの動作の理由を知りたいです。

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
行動の違いは何ですか?
Mong Zhu

4
Javaの場合、コンパイラーは 'compile'変数の更新を認識しないと想定します。変数宣言に「volatile」を追加すると、それが修正されます(それを静的フィールドにします)。
セバスチャン


4
注:これが、マルチスレッド開発にmutexやアトミック操作などがある理由です。マルチスレッド化を開始するときは、以前は明らかではなかった、一連の注目すべき追加のメモリ問題を考慮する必要があります。ミューテックスのようなスレッド同期ツールはこれを解決します。
Cort Ammon 2017年

5
@DavidSchwartz:確かに許可されています。また、コンパイラ、ランタイム、CPUがそのコードの結果を予想とは異なるものにすることが許可されています。特に、C#はboolアクセスを非アトミックにすることは許可されていませんが、不揮発性読み取りを時間的に後方に移動することは許可されています。対照的に、ダブルスには原子性に対するそのような制限はありません。同期化されていない2つの異なるスレッドでの二重の読み取りと書き込みは、切り離すことができます。
Eric Lippert、2017年

回答:


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クラスを処理するときに、メソッドの開始時に実際にいくつかのレジスターまたはスタックフレームに値をキャッシュし、メソッドが終了するまで決して更新しないと想像できます。これは、「= true」が実行される前にスレッドとメソッドが終了しないという保証がないためです。保証がない場合は、キャッシュせずに、ヒープオブジェクトを毎回読み取る代わりに1回読み取ることでパフォーマンスを向上させてください。反復。falseBody()

これがまさにキーワードvolatileが存在する理由です。

このヘルパークラスは、であるためには、正しい 小さな少し良く 1)マルチスレッド環境では、それが持っている必要があります。

    public volatile bool isComplete = false;

もちろん、自動生成されたコードなので、追加することはできません。より良いアプローチは、へのlock()読み取りと書き込みの周りにs を追加するかisCompleted、またはベアメタル(ベアメタルではない)を実行する代わりに、すぐに使用できる他の同期またはスレッド/タスクユーティリティを使用することです。それは、GC、JIT、および(..)を使用したCLR上のC#であるためです。

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

ところで。それは私の推測です。コンパイルするつもりもありませんでした。

ところで。バグではないようです。それは非常にあいまいな副作用のようなものです。また、私がそれについて正しい場合、それは言語の欠陥である可能性があります-C#は、キャプチャーされ、クロージャーのメンバーフィールドに昇格されるローカル変数に「volatile」キーワードを配置できるようにする必要があります。

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


2
@EricLippert:おっと、とても早く確認してくれてありがとう!あなたはどう思いますか、将来のバージョンでvolatile、キャプチャして閉じるローカル変数のオプションを取得できる可能性はありますか?私は...それは、コンパイラによってプロセスに少し困難なことが想像
ケツァルコアトル

7
@quetzalcoatl:すぐに追加される機能は期待していません。これは、あなたがしたいと思いますコーディングの一種で落胆はなく、容易になります。さらに、ものを揮発性にすることは必ずしもすべての問題を解決するわけではありません。以下は、すべてが揮発性でプログラムがまだ間違っている例です。バグを見つけることができますか?blog.coverity.com/2014/03/26/reordering-optimizations
Eric Lippert

3
わかった。私はマルチスレッドの最適化を理解しようとするのをあきらめます...それがどれほど複雑であるかは正気ではありません。
2017年

10
@Pikoh:もう一度、オプティマイザのように考えます。インクリメントされるが読み取られない変数があります。読み込まれない変数は完全に削除できます。
Eric Lippert 2017年

4
@EricLippert今私の心はクリックをしました。このスレッドは非常に有益でした。本当にありがとうございました。
ピコ

82

ケツァルコアトル答えは正しいです。それにもっと光を当てるには:

C#コンパイラとCLRジッターは、現在のスレッドが実行中の唯一のスレッドであると想定して、非常に多くの最適化を行うことができます。現在のスレッドが実行中の唯一のスレッドではない世界で、これらの最適化によってプログラムが不正確になった場合、それは問題です。コンパイラーに指示し、実行しているクレイジーなマルチスレッド化されたものにジッターを与えるマルチスレッド化されたプログラムを作成する必要あります。

この特定のケースでは、ジッターは許可されていますが、必須ではありませんが、変数がループ本体によって変更されていないことを確認し、その結果を結論付けます。変更されない場合は、ループのたびにではなく、変数の真偽を一度チェックする必要があります。そして、これは実際に起こっていることです。

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


1
コメントは拡張ディスカッション用ではありません。この会話はチャットに移動さました
マダラのゴースト

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)は最初にロードされ、更新されません。


8

実際には答えではありませんが、問題をさらに解明するために:

問題は、ときであるように思わiラムダ本体の中で宣言された、それはだだけ代入式で読み取ります。それ以外の場合、コードはリリースモードで適切に機能します。

  1. i ラムダ本体の外で宣言されています:

    int i = 0; // Declared outside the lambda body
    
    var t = new Thread(() =>
    {
        while (!isComplete) { i += 0; }
    }); // Completes in release mode
  2. i 代入式で読み取られません:

    var t = new Thread(() =>
    {
        int i = 0;
        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

私の賭けは、いくつかのコンパイラまたはJITの最適化に関するiものです。私より賢い人なら、この問題にもっと光を当てることができるでしょう。

それでも、同様のコードが実際に目的を果たす場所を確認できないため、あまり心配する必要はありません。


1
..私はかなり確信して、それは(実際には、後に閉鎖にメンバフィールドに昇格ます)ローカル変数に追加するthatcannot「揮発性」キーワードについてのすべてだよ、私の答えを参照してください
ケツァルコアトル
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.