質問
私は、複数のスレッドが変数にアクセスできる場合、プロセッサが途中で別のスレッドに切り替わる可能性があるため、その変数に対するすべての読み取りと書き込みは「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 フィールドがロックの外で読み取られるのはなぜですか?別のスレッドが同時に書き込もうとしている可能性はありませんか? 変数の読み取りと書き込みはアトミックですか?
解決
最終的な答えについては、仕様を参照してください。:)
CLI 仕様のパーティション I、セクション 12.6.6 には次のように記載されています。「準拠する CLI は、ある位置へのすべての書き込みアクセスが同じサイズである場合、ネイティブ ワード サイズ以下の適切に配置されたメモリ位置への読み取りおよび書き込みアクセスがアトミックであることを保証するものとします。」
これにより、s_Initialized が不安定になることはなく、32 ビットより小さいプリミティブ型への読み取りと書き込みがアトミックであることが確認できます。
特に、 double
そして long
(Int64
そして UInt64
) は ない 32 ビット プラットフォームではアトミックであることが保証されます。のメソッドを使用できます。 Interlocked
これらを守るためのクラスです。
さらに、読み取りと書き込みはアトミックですが、プリミティブ型は読み取り、操作、および再書き込みを行う必要があるため、加算、減算、増加および減少するプリミティブ型では競合状態が発生します。インターロック クラスを使用すると、これらを使用して保護できます。 CompareExchange
そして Increment
メソッド。
インターロックによりメモリ バリアが作成され、プロセッサによる読み取りと書き込みの順序変更が防止されます。この例では、ロックが唯一の必要なバリアを作成します。
他のヒント
これは、ダブル チェック ロック パターンの (悪い) 形式です。 ない 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_Provider の読み取りを s_Initialized の読み取りの前に移動することは完全に正当です。
s_Initialized が揮発性である場合、s_Provider の読み取りは s_Initialized の読み取りの前に移動することはできません。また、s_Initialized が true に設定されてすべてが正常になった後は、プロバイダーの初期化も移動できません。
Joe Duffy もこの問題について次の記事を書きました。 二重チェックされたロックの壊れたバリアント
ちょっと待ってください -- タイトルにある質問は、Rory が尋ねている本当の質問ではありません。
名ばかりの質問には「いいえ」という単純な答えがありますが、実際の質問を見ると、これはまったく役に立ちません。これに簡単に答えた人はいないと思います。
ロリーが尋ねる本当の質問はずっと後になって提示され、彼が挙げた例により適切です。
なぜs_initializedフィールドがロックの外で読まれるのですか?
これに対する答えも簡単ですが、変数アクセスのアトミック性とはまったく関係ありません。
s_Initialized フィールドはロックの外側で読み取られます。 ロックは高価です.
s_Initialized フィールドは基本的に「一度だけ書き込み」であるため、誤検知を返すことはありません。
ロックの外で読み取ると経済的です。
これは 低コスト との活動 高い 恩恵を受けるチャンス。
これが、ロックの外側で読み取られる理由です。指定されない限り、ロックを使用するコストの支払いを避けるためです。
ロックが安価であれば、コードはより単純になり、最初のチェックは省略されます。
(編集:ロリーからの素晴らしい反応が続きます。そうです、ブール値の読み取りは非常にアトミックです。誰かが非アトミックなブール値読み取りを備えたプロセッサを構築した場合、それは DailyWTF で紹介されるでしょう。)
正解は「はい、ほとんどです」のようです。
- CLI 仕様を参照している John の回答は、32 ビット プロセッサ上の 32 ビット以下の変数へのアクセスはアトミックであることを示しています。
C# 仕様のセクション 5.5 でさらに確認してください。 変数参照の原子性:
次のデータ型の読み取りと書き込みはアトミックです。bool、char、byte、sbyte、short、ushort、uint、int、float、参照型。さらに、前のリストの基礎となる型を持つ列挙型の読み取りと書き込みもアトミックです。ユーザー定義型だけでなく、long、ulong、double、および 10 進数を含む他の型の読み取りと書き込みは、アトミックであることが保証されません。
私の例のコードは、ASP.NET チーム自身が書いたように Membership クラスを言い換えたものであるため、s_Initialized フィールドへのアクセス方法が正しいと常に想定して大丈夫です。今ではその理由が分かりました。
編集:Thomas Dannecker が指摘しているように、フィールドへのアクセスがアトミックであっても、実際には s_Initialized をマークする必要があります。 揮発性の プロセッサが読み取りと書き込みの順序を変更することによってロックが解除されないようにします。
初期化機能に異常があります。次のようになります。
private static void Initialize()
{
if(s_initialized)
return;
lock(s_lock)
{
if(s_Initialized)
return;
s_Initialized = true;
}
}
ロック内の 2 番目のチェックがないと、初期化コードが 2 回実行される可能性があります。したがって、最初のチェックは、不必要なロックの取得を避けるためのパフォーマンスに関するものであり、2 番目のチェックは、スレッドが初期化コードを実行しているが、まだ設定されていない場合のチェックです。 s_Initialized
フラグを設定すると、2 番目のスレッドが最初のチェックに合格し、ロックで待機することになります。
変数の読み取りと書き込みはアトミックではありません。アトミックな読み取り/書き込みをエミュレートするには、同期 API を使用する必要があります。
この問題や同時実行に関するその他の多くの問題に関する素晴らしい参考情報については、Joe Duffy の書籍を必ず入手してください。 最新の光景. 。リッパーだ!
「C# での変数へのアクセスはアトミック操作ですか?」
いいえ。そして、これは C# の問題でも、.net の問題でもなく、プロセッサの問題です。
OJ は、この種の情報を得るにはジョー ダフィーが最適であると指摘しています。さらに、「インターロック」は、さらに詳しく知りたい場合に最適な検索用語です。
「読み取りの読み取り」は、フィールドの合計がポインタのサイズを超える任意の値で発生する可能性があります。
@レオン
あなたの言いたいことはわかります。私が質問し、コメントした方法から、この質問はいくつかの異なる方法で受け取ることができます。
明確にするために、明示的な同期コードを使用せずに同時スレッドでブール型フィールドの読み書きを行うのが安全かどうか、つまり、ブール型 (または他のプリミティブ型の) アトミック変数にアクセスするのが安全かどうかを知りたかったのです。
次に、メンバーシップ コードを使用して具体的な例を示しましたが、二重チェック ロック、s_Initialized が 1 回しか設定されないという事実、初期化コード自体をコメントアウトしたことなど、多くの注意をそらすものを導入しました。
悪いです。
s_Initialized を volatile キーワードで装飾し、ロックの使用を完全に省略することもできます。
それは正しくありません。最初のスレッドがフラグを設定する前に 2 番目のスレッドがチェックに合格するという問題が依然として発生し、初期化コードが複数回実行されることになります。
あなたはこう尋ねていると思います s_Initialized
ロック外で読み取られた場合、不安定な状態になる可能性があります。簡単に言うと「ノー」です。単純な代入/読み取りは、考えられるすべてのプロセッサでアトミックな単一のアセンブリ命令に要約されます。
64 ビット変数への代入がどのような場合なのかはわかりません。それはプロセッサによって異なります。アトミックではないと思いますが、おそらく最新の 32 ビット プロセッサ、そしてすべての 64 ビット プロセッサで可能です。複合値タイプの割り当てはアトミックではありません。
私はそれらがそうであると思いました - 同時に s_Provider に対して何かを実行していない限り、あなたの例のロックのポイントはわかりません - そうすれば、ロックはこれらの呼び出しが同時に発生することを保証します。
それはありますか //Perform initialization
コメント カバー s_Provider を作成していますか?例えば
private static void Initialize()
{
if (s_Initialized)
return;
lock(s_lock)
{
s_Provider = new MembershipProvider ( ... )
s_Initialized = true;
}
}
それ以外の場合は、静的 property-get はとにかく null を返すだけです。
コードが常に弱く順序付けされたアーキテクチャで動作するようにするには、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 を更新する前に、コンストラクターの書き込みがグローバルに可視であることを確認する必要があります。 、ロックの外側でテストされます。
アン If (itisso) {
ブールのチェックはアトミックですが、最初のチェックをロックする必要がない場合でも。
いずれかのスレッドが初期化を完了している場合、それは true になります。複数のスレッドが同時にチェックされていても問題ありません。全員が同じ答えを得るでしょう、そして、衝突は起こりません。
別のスレッドが最初にロックを取得し、すでに初期化プロセスを完了している可能性があるため、ロック内の 2 番目のチェックが必要です。
あなたが尋ねているのは、メソッド内のフィールドにアトミックに複数回アクセスするかどうかですが、答えはノーです。
上の例では、複数の初期化が行われる可能性があるため、初期化ルーチンに欠陥があります。を確認する必要があります。 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;
}
}
ああ、気にしないで...指摘されているように、これは確かに間違いです。2 番目のスレッドが「初期化」コード セクションに入るのを妨げるものではありません。ああ。
s_Initialized を volatile キーワードで装飾し、ロックの使用を完全に省略することもできます。