2

私は次のコードを持っています:

    public static System.Int32 i = 0;

    static void Main(string[] args)
    {
        new Thread(Worker1) { IsBackground = true }.Start();
        new Thread(Worker2) { IsBackground = true }.Start();

        Console.WriteLine("Running... Press enter to quit");
        Console.ReadLine();
    }

    static void Worker1(object _)
    {
        while (true)
        {
            var oldValue = i;
            i++;
            var newValue = i;

            if (newValue < oldValue)
                Console.WriteLine("{2}, i++ went backwards! oldValue={0}, newValue={1}!", oldValue, newValue, DateTime.Now.ToString("HH:MM:ss.fff"));
        }
    }

    static void Worker2(object _)
    {
        while (true)
        {
            i++;
        }
    }

実行すると、次のような出力が生成されます。

17:08:17.020, i++ went backwards! oldValue=6124653, newValue=6113984!
17:08:17.057, i++ went backwards! oldValue=18764535, newValue=18752368!
17:08:17.086, i++ went backwards! oldValue=27236177, newValue=27236176!
17:08:17.087, i++ went backwards! oldValue=27550457, newValue=27535008!
17:08:17.130, i++ went backwards! oldValue=40251349, newValue=40235492!
17:08:17.137, i++ went backwards! oldValue=42339974, newValue=42323786!
17:08:17.142, i++ went backwards! oldValue=43828602, newValue=43828436!
17:08:17.149, i++ went backwards! oldValue=45969702, newValue=45959111!
17:08:17.158, i++ went backwards! oldValue=48705847, newValue=48705549!
17:08:17.230, i++ went backwards! oldValue=71199684, newValue=71199674!

注:これは、Windows7でハイパースレッディングを使用するクアッドコアi7で実行されています

私が知る限り、次のいずれかです。

  • Thread2は、oldValueとnewValueの読み取りの間に、iを約40億倍(完全ではありません!)インクリメントしています。上記の数字とタイミングを考えると、次の2秒間に10回宝くじに当選する可能性が高いようです。

  • CPUとコンパイラがいくつかの並べ替えを行っています...これは論理的な説明のようですが、実際にこれを引き起こす可能性のある一連の操作を完全に理解することはできませんか?

誰かがそれに光を当てることができますか?


質問を明確にするために:私は、教育演習の一環として、メモリオーダリングのバグを再現するコードを意図的に探しています。これを修正する方法はたくさんありますが、私は主に何が起こっているのかを分析することに興味がありました。

@Tymekは、以下に(指摘されたので、目がくらむほど明白な)答えを示しました。

4

3 に答える 3

4

i ++はアトミックではなく、実際には次のようになります。

  • iメモリからレジスタに読み取り、
  • レジスタをインクリメントし、
  • レジスタをメモリに書き込みます。ここiで、
  • 戻るi;

このシーケンスは、どの段階でも中断できます。

これを念頭に置いて、運命のメッセージを生成する1つのケースは次のとおりです。

  • なりましょi100;
  • スレッド2100が読み取り、中断されます。
  • スレッド1はしばらくの間実行され、iを120にインクリメントし(完全なループはほとんどありません)、次にiとして読み取り120、読み取りiとインクリメントを行い、;の121前に中断されます。var newValue = i;
  • スレッド2はに増分i101、中断されます。
  • スレッド1が実行され、実行されますvar newValue = i<-これは読み込まれ101ます;

oldValue120そうnewValueです101

于 2012-08-02T08:11:53.417 に答える
1

古い値の割り当て、iのインクリメント、および新しい値の割り当てが「同時に」行われるようにする場合は、ロックを使用する必要があります。プライベート変数を導入してから、これにロックを使用するだけです。これは次のようになります。

lock(lockObj)
{
  var oldValue = i;             
  i++;             
  var newValue = i;
}
于 2012-08-02T06:13:36.020 に答える
1

この問題を回避するには、Interlocked.Incrementを使用して、操作がアトミックでスレッドセーフであることを保証します。i ++を使用する場合、それはアトミックではなく、インクリメント中にその値が変更される可能性があります(たとえば、別のスレッドから)。

于 2012-08-02T06:28:49.923 に答える