14

私はまだ少し不明確で、コードをいつロックするかはわかりません。私の一般的な経験則は、静的変数の読み取りまたは書き込み時に操作をロックでラップすることです。しかし、静的変数が読み取り専用の場合 (たとえば、型の初期化中に設定された読み取り専用の場合)、それにアクセスするために lock ステートメントでラップする必要はありませんよね? 最近、次の例のようなコードをいくつか見たので、マルチスレッドに関する知識にギャップがあるのではないかと思いました。

class Foo
{
    private static readonly string bar = "O_o";

    private bool TrySomething()
    {
        string bar;

        lock(Foo.objectToLockOn)
        {
            bar = Foo.bar;          
        }       

        // Do something with bar
    }
}

それは私には意味がありません.なぜレジスタの読み取りに並行性の問題があるのでしょうか?

また、この例は別の問題を提起します。これらのうちの1つは他のものより優れていますか? (たとえば、例 2 では、ロックを保持する時間が短くなりますか?) MSIL を逆アセンブルできると思います...

class Foo
{
    private static string joke = "yo momma";

    private string GetJoke()
    {
        lock(Foo.objectToLockOn)
        {
            return Foo.joke;
        }
    }
}

対。

class Foo
{
    private static string joke = "yo momma";

        private string GetJoke()
        {
            string joke;

            lock(Foo.objectToLockOn)
            {
                joke = Foo.joke;
            }

            return joke;
        }
}
4

7 に答える 7

23

あなたが書いたコードはどれも初期化後に static フィールドを変更しないので、ロックする必要はありません。文字列を新しい値に置き換えるだけでも、新しい値が古い値の読み取り結果に依存しない限り、同期は必要ありません。

同期が必要なのは静的フィールドだけではありません。変更可能な共有参照は、同期の問題に対して脆弱です。

class Foo
{
    private int count = 0;
    public void TrySomething()    
    {
        count++;
    }
}

TrySomething メソッドを実行する 2 つのスレッドは問題ないと考えるかもしれません。しかし、そうではありません。

  1. スレッド A は、カウント (0) の値をレジスターに読み取り、インクリメントできるようにします。
  2. コンテキストスイッチ!スレッド スケジューラは、スレッド A に十分な実行時間があったと判断します。次はスレッド B です。
  3. スレッド B は、count (0) の値をレジスターに読み取ります。
  4. スレッド B はレジスタをインクリメントします。
  5. スレッド B は結果 (1) を count に保存します。
  6. コンテキスト スイッチを A に戻します。
  7. スレッド A は、スタックに保存されたカウント (0) の値でレジスタを再ロードします。
  8. スレッド A はレジスタをインクリメントします。
  9. スレッド A は結果 (1) を count に保存します。

つまり、count++ を 2 回呼び出したにもかかわらず、count の値が 0 から 1 になっただけです。コードをスレッドセーフにしましょう。

class Foo
{
    private int count = 0;
    private readonly object sync = new object();
    public void TrySomething()    
    {
        lock(sync)
            count++;
    }
}

スレッド A が中断されると、スレッド B はロック ステートメントにヒットし、スレッド A が同期を解放するまでブロックするため、カウントを台無しにすることはできません。

ちなみに、インクリメントする Int32 と Int64 をスレッドセーフにする別の方法があります。

class Foo
{
    private int count = 0;
    public void TrySomething()    
    {
        System.Threading.Interlocked.Increment(ref count);
    }
}

質問の 2 番目の部分については、読みやすい方を使用すると思いますが、パフォーマンスの違いはごくわずかです。初期の最適化は諸悪の根源など

スレッド化が難しい理由

于 2008-09-19T21:10:36.110 に答える
8

32 ビット以下のフィールドの読み取りまたは書き込みは、C# ではアトミック操作です。私が見る限り、提示されたコードをロックする必要はありません。

于 2008-09-19T20:24:58.683 に答える
3

あなたの最初のケースでは、ロックは必要ないように思えます。静的初期化子を使用して bar を初期化すると、スレッドセーフであることが保証されます。値を読み取るだけなので、ロックする必要はありません。値が変更されない場合、競合は発生しません。なぜロックする必要があるのでしょうか。

于 2008-09-19T20:26:42.423 に答える
1

ダーティリード?

于 2008-09-19T20:22:20.143 に答える
1

ポインタに値を書き込むだけの場合は、そのアクションがアトミックであるため、ロックする必要はありません。一般に、開始と終了の間で状態が変化しないことに依存する少なくとも 2 つのアトミック アクション (読み取りまたは書き込み) を含むトランザクションを実行する必要がある場合は、常にロックする必要があります。

とはいえ、私は変数のすべての読み取りと書き込みがアトミック アクションである Java の世界から来ました。ここでの他の回答は、.NETが異なることを示唆しています。

于 2008-09-19T20:21:04.420 に答える
1

私の意見では、異なるスレッドからの読み取り/書き込みが必要な位置に静的変数を配置しないように非常に努力する必要があります。その場合、それらは本質的に自由に使えるグローバル変数であり、グローバルはほとんど常に悪いことです。

そうは言っても、静的変数をそのような位置に置く場合は、念のために読み取り中にロックしたいかもしれません-別のスレッドが急降下して読み取りに値を変更した可能性があることを覚えておいてください。破損したデータになる可能性があります。読み取りは、ロックによるものでない限り、必ずしもアトミック操作ではありません。書き込みと同じ - 常にアトミック操作であるとは限りません。

編集: マークが指摘したように、C# 読み取りの特定のプリミティブは常にアトミックです。ただし、他のデータ型には注意してください。

于 2008-09-19T20:24:59.980 に答える
0

「どちらが優れているか」という質問については、関数スコープが他に使用されていないため、同じです。

于 2008-09-19T20:25:03.473 に答える