65

複数のスレッドが変数にアクセスできる場合、プロセッサが途中で別のスレッドに切り替わる可能性があるため、その変数からのすべての読み取りとその変数への書き込みは、「lock」ステートメントなどの同期コードで保護する必要があると私は信じています。書き込み。

ただし、Reflectorを使用してSystem.Web.Security.Membershipを調べていたところ、次のようなコードが見つかりました。

public static class Membership
{
    private static bool s_Initialized = false;
    private static object s_lock = new object();
    private static MembershipProvider s_Provider;

    public static MembershipProvider Provider
    {
        get
        {
            Initialize();
            return s_Provider;
        }
    }

    private static void Initialize()
    {
        if (s_Initialized)
            return;

        lock(s_lock)
        {
            if (s_Initialized)
                return;

            // Perform initialization...
            s_Initialized = true;
        }
    }
}

s_Initializedフィールドがロックの外部で読み取られるのはなぜですか?別のスレッドが同時にそれに書き込もうとしていませんか?変数の読み取りと書き込みはアトミックですか?

4

15 に答える 15

37

決定的な答えについては、仕様を参照してください。:)

パーティション I、CLI 仕様のセクション 12.6.6 は次のように述べています。 ."

したがって、s_Initialized が不安定になることはなく、32 ビットより小さいプリミティブ型への読み取りと書き込みはアトミックであることが確認されます。

特に、doubleand long(Int64およびUInt64) は、32 ビット プラットフォームではアトミックであることが保証されていません。クラスのメソッドを使用して、Interlockedこれらを保護できます。

さらに、読み取りと書き込みはアトミックですが、プリミティブ型の加算、減算、およびインクリメントとデクリメントには、読み取り、操作、および再書き込みが必要なため、競合状態があります。インターロック クラスを使用すると、CompareExchangeおよびIncrementメソッドを使用してこれらを保護できます。

インターロックはメモリ バリアを作成し、プロセッサが読み取りと書き込みの順序を変更するのを防ぎます。ロックは、この例で唯一必要なバリアを作成します。

于 2008-08-13T13:24:41.533 に答える
34

これは、C# ではスレッド セーフではないダブル チェック ロック パターンの (悪い) 形式です!

このコードには 1 つの大きな問題があります。

s_Initialized は揮発性ではありません。つまり、s_Initialized が true に設定された後、初期化コードの書き込みが移動でき、他のスレッドは、s_Initialized が true であっても、初期化されていないコードを見ることができます。すべての書き込みは揮発性の書き込みであるため、これは Microsoft のフレームワークの実装には適用されません。

しかし、Microsoft の実装でも、初期化されていないデータの読み取りを並べ替えることができる (つまり、CPU によってプリフェッチされる) ため、s_Initialized が true の場合、初期化する必要があるデータを読み取ると、キャッシュ ヒット (つまり、 . 読み取りが並べ替えられます)。

例えば:

Thread 1 reads s_Provider (which is null)  
Thread 2 initializes the data  
Thread 2 sets s\_Initialized to true  
Thread 1 reads s\_Initialized (which is true now)  
Thread 1 uses the previously read Provider and gets a NullReferenceException

s_Initialized の読み取りの前に s_Provider の読み取りを移動することは、どこにも揮発性の読み取りがないため、完全に合法です。

s_Initialized が揮発性である場合、s_Initialized の読み取り前に s_Provider の読み取りを移動することはできません。また、s_Initialized が true に設定された後、プロバイダーの初期化を移動することは許可されず、すべて問題ありません。

Joe Duffy は、この問題について次の記事も書いています。

于 2008-09-16T15:50:29.737 に答える
12

ちょっと待ってください -- タイトルにある質問は、Rory が尋ねている本当の質問ではありません。

タイトルの質問には「いいえ」という単純な答えがあります-しかし、実際の質問を見ると、これはまったく役に立ちません-誰も簡単な答えを与えていないと思います.

Rory が尋ねる本当の質問は、ずっと後で提示され、彼が示す例により適切です。

s_Initialized フィールドがロックの外で読み取られるのはなぜですか?

これに対する答えも単純ですが、変数アクセスの原子性とはまったく関係ありません。

ロックは高価であるため、s_Initialized フィールドはロックの外で読み取られます。

s_Initialized フィールドは基本的に「一度だけ書き込む」ため、誤検知を返すことはありません。

ロックの外で読むのが経済的です。

これは、メリットが得られる可能性が高い低コストのアクティビティです。

これが、ロックの外側で読み取られる理由です。指定されていない限り、ロックを使用するコストを支払うことを避けるためです。

ロックが安価であれば、コードはより単純になり、最初のチェックは省略されます。

(編集: rory からの素晴らしい応答が続きます。ええ、ブール値の読み取りは非常にアトミックです。誰かが非アトミックなブール値の読み取りを使用してプロセッサを構築した場合、それらは DailyWTF で取り上げられます。)

于 2008-08-15T12:53:51.250 に答える
7

正解は「はい、ほとんど」のようです。

  1. CLI仕様を参照するJohnの回答は、32ビットプロセッサで32ビット以下の変数へのアクセスはアトミックであることを示しています。
  2. C#仕様、セクション5.5、変数参照のAtomicityからのさらなる確認:

次のデータ型の読み取りと書き込みはアトミックです:bool、char、byte、sbyte、short、ushort、uint、int、float、および参照型。さらに、前のリストの基になる型を持つ列挙型の読み取りと書き込みもアトミックです。long、ulong、double、decimalなどの他のタイプの読み取りと書き込み、およびユーザー定義タイプは、アトミックであることが保証されていません。

  1. 私の例のコードは、ASP.NETチーム自身によって記述されたように、Membershipクラスから言い換えられたため、s_Initializedフィールドにアクセスする方法が正しいと常に想定して安全でした。今、私たちはその理由を知っています。

編集:Thomas Daneckerが指摘しているように、フィールドへのアクセスはアトミックですが、プロセッサが読み取りと書き込みを並べ替えることによってロックが解除されないように、s_Initializedは実際には揮発性としてマークする必要があります。

于 2008-08-13T17:22:04.330 に答える
2

初期化機能に障害があります。次のようになります。

private static void Initialize()
{
    if(s_initialized)
        return;

    lock(s_lock)
    {
        if(s_Initialized)
            return;
        s_Initialized = true;
    }
}

ロック内の2番目のチェックがないと、初期化コードが2回実行される可能性があります。したがって、最初のチェックは、不必要にロックを取得する手間を省くためのパフォーマンスです。2番目のチェックは、スレッドが初期化コードを実行しているが、まだs_Initializedフラグを設定していないため、2番目のスレッドが最初のチェックに合格してロックで待っています。

于 2008-08-13T12:08:29.220 に答える
1

s_Initializedロックの外で読んだときに不安定な状態になる可能性があるかどうかを尋ねていると思います。簡単な答えはノーです。単純な割り当て/読み取りは、私が考えることができるすべてのプロセッサでアトミックな単一のアセンブリ命令に要約されます。

64ビット変数への割り当てがどのような場合かはわかりません。プロセッサによって異なります。アトミックではないと思いますが、おそらく最新の32ビットプロセッサとすべての64ビットプロセッサにあります。複素数値タイプの割り当てはアトミックではありません。

于 2008-08-13T12:34:20.057 に答える
1

変数の読み取りと書き込みはアトミックではありません。アトミックな読み取り/書き込みをエミュレートするには、同期APIを使用する必要があります。

これに関するすばらしい参考資料や、並行性に関するその他の多くの問題については、JoeDuffyの最新のスペクタクルのコピーを必ず入手してください。リッパーです!

于 2008-08-13T12:35:12.897 に答える
1

「C#で変数にアクセスすることは不可分操作ですか?」

いいえ。そして、それはC#のことでも、.netのことでもありません。それは、プロセッサーのことです。

OJは、ジョー・ダフィーがこの種の情報を得るために行く人であることにスポットを当てています。「インターロック」は、詳細を知りたい場合に使用するのに最適な検索用語です。

「引き裂かれた読み取り」は、フィールドの合計がポインターのサイズを超える任意の値で発生する可能性があります。

于 2008-08-13T12:39:33.883 に答える
1

また、s_Initialized を volatile キーワードでデコレートし、ロックの使用を完全に控えることもできます。

それは正しくありません。最初のスレッドがフラグを設定する機会を得る前に、2 番目のスレッドがチェックを通過するという問題が発生し、初期化コードが複数回実行されます。

于 2008-08-13T13:28:47.013 に答える
1

ブール値のIf (itisso) {チェックはアトミックですが、そうでない場合でも最初のチェックをロックする必要はありません。

いずれかのスレッドが初期化を完了した場合、それは true になります。複数のスレッドが同時にチェックしていても問題ありません。彼らは皆同じ​​答えを得るでしょう、そして、衝突はありません.

別のスレッドが最初にロックを取得し、初期化プロセスをすでに完了している可能性があるため、ロック内の 2 番目のチェックが必要です。

于 2013-02-28T01:35:36.470 に答える
0

私はそれらがそうだと思った-あなたが同時にs_Providerにも何かをしているのでない限り、あなたの例のロックのポイントはわからない-そしてロックはこれらの呼び出しが一緒に起こったことを確実にするだろう。

その//Perform initializationコメントはs_Providerの作成をカバーしていますか?例えば

private static void Initialize()
{
    if (s_Initialized)
        return;

    lock(s_lock)
    {
        s_Provider = new MembershipProvider ( ... )
        s_Initialized = true;
    }
}

それ以外の場合、その静的プロパティ-getはとにかくnullを返すだけです。

于 2008-08-13T12:00:51.343 に答える
0

あなたが求めているのは、メソッドのフィールドに複数回アトミックにアクセスするかどうかです-答えはノーです。

上記の例では、複数の初期化が発生する可能性があるため、初期化ルーチンに障害があります。複数のスレッドが実際に初期化コードを実行する前にフラグをs_Initialized読み取る競合状態を防ぐために、ロックの内側と外側でフラグをチェックする必要があります。s_Initialized例えば、

private static void Initialize()
{
    if (s_Initialized)
        return;

    lock(s_lock)
    {
        if (s_Initialized)
            return;
        s_Provider = new MembershipProvider ( ... )
        s_Initialized = true;
    }
}
于 2008-08-13T12:16:39.860 に答える
0

おそらくInterlockedが手がかりを与えます。それ以外の場合、これはかなり良いです。

私は彼らがアトミックではないと推測したでしょう。

于 2008-08-13T12:19:22.663 に答える
0

コードが常に弱く順序付けられたアーキテクチャで機能するようにするには、s_Initialized を記述する前に MemoryBarrier を配置する必要があります。

s_Provider = new MemershipProvider;

// MUST PUT BARRIER HERE to make sure the memory writes from the assignment
// and the constructor have been wriitten to memory
// BEFORE the write to s_Initialized!
Thread.MemoryBarrier();

// Now that we've guaranteed that the writes above
// will be globally first, set the flag
s_Initialized = true;

MembershipProvider コンストラクターで発生するメモリ書き込みと s_Provider への書き込みは、弱い順序付けのプロセッサで s_Initialized に書き込む前に発生する保証はありません。

このスレッドでの多くの考えは、何かがアトミックかどうかについてです。それは問題ではありません。問題は、スレッドの書き込みが他のスレッドに見える順序です。弱い順序付けのアーキテクチャでは、メモリへの書き込みは順序どおりに行われず、それが実際の問題であり、変数がデータ バス内に収まるかどうかではありません。

編集:実際、私はステートメントにプラットフォームを混在させています。C# の CLR 仕様では、(必要に応じてストアごとに高価なストア命令を使用することによって) 書き込みがグローバルに可視であり、順序どおりである必要があります。したがって、実際にそのメモリバリアをそこに置く必要はありません。ただし、C または C++ で、グローバルな可視性の順序の保証が存在せず、ターゲット プラットフォームのメモリの順序が弱く、マルチスレッドである場合は、s_Initialized を更新する前に、コンストラクタの書き込みがグローバルに可視であることを確認する必要があります。 、ロックの外でテストされます。

于 2012-08-30T22:59:27.060 に答える
-1

ああ、気にしないでください...指摘されたように、これは確かに間違っています。2番目のスレッドが「初期化」コードセクションに入るのを妨げることはありません。ああ。

s_Initializedをvolatileキーワードで装飾し、ロックの使用を完全にやめることもできます。

于 2008-08-13T12:42:16.720 に答える