11

私が非常に不変な型を持っている場合 (すべてのメンバーは読み取り専用であり、それらが参照型のメンバーである場合、それらは非常に不変なオブジェクトも参照します)。

次のように、型に遅延初期化プロパティを実装したいと思います。

private ReadOnlyCollection<SomeImmutableType> m_PropName = null;
public ReadOnlyCollection<SomeImmutableType> PropName
{
    get
    {
        if(null == m_PropName)
        {
            ReadOnlyCollection<SomeImmutableType> temp = /* do lazy init */;
            m_PropName = temp;
        }
        return m_PropName;
    }
}

私が言えることから:

m_PropName = temp; 

...スレッドセーフです。両方のスレッドが同時に初期化するために競合することについてはあまり心配していません。これはめったにないためです。論理的な観点からは両方の結果が同じであり、ロックがない場合はロックを使用したくないからです。に。

これは機能しますか?長所と短所は何ですか?

編集: 回答ありがとうございます。おそらくロックを使用して先に進みます。しかし、コンパイラが temp 変数が不要であることを認識し、m_PropName に直接代入する可能性を誰も持ち出さなかったことに驚いています。その場合、読み取りスレッドは、構築が完了していないオブジェクトを読み取る可能性があります。コンパイラはそのような状況を防ぎますか?

(回答は、ランタイムがこれを許可しないことを示しているようです。)

編集:そこで、Joe Duffy によるこの記事に 触発された Interlocked CompareExchange メソッドを使用することにしました。

基本的:

private ReadOnlyCollection<SomeImmutableType> m_PropName = null;
public ReadOnlyCollection<SomeImmutableType> PropName
{
    get
    {
        if(null == m_PropName)
        {
            ReadOnlyCollection<SomeImmutableType> temp = /* do lazy init */;
            System.Threading.Interlocked(ref m_PropName, temp, null);
        }
        return m_PropName;
    }
}

これにより、このオブジェクト インスタンスでこのメソッドを呼び出すすべてのスレッドが同じオブジェクトへの参照を取得するようになるため、== 演算子が機能します。無駄な作業が発生する可能性がありますが、これは問題ありません。楽観的なアルゴリズムになるだけです。

以下のコメントにあるように、これは動作する .NET 2.0 メモリ モデルに依存します。それ以外の場合、m_PropName は volatile と宣言する必要があります。

4

8 に答える 8

6

それはうまくいきます。仕様のセクション 5.5 で説明されているように、C# での参照への書き込みはアトミックであることが保証されています。これはおそらく良い方法ではありません。なぜなら、パフォーマンスへのおそらく小さな影響と引き換えに、コードのデバッグと読み取りがより混乱するからです。

Jon Skeet には、C# でのシングルトンの実装に関する優れたページがあります。

このような小さな最適化に関する一般的なアドバイスは、プロファイラーがこのコードがホットスポットであると通知しない限り、実行しないことです。また、仕様を確認しないとほとんどのプログラマーが完全に理解できないコードを書くことにも注意が必要です。

編集: コメントに記載されているように、オブジェクトの 2 つのバージョンが作成されてもかまわないと言っていますが、その状況は直感に反するため、このアプローチは決して使用しないでください。

于 2009-03-16T21:33:57.310 に答える
5

これに対する他の回答を聞きたいと思いますが、問題はありません。重複コピーは破棄され、GC されます。

ただし、フィールドを作成する必要がありvolatileます。

これに関して:

しかし、コンパイラが temp 変数が不要であることを認識し、m_PropName に直接代入する可能性を誰も持ち出さなかったことに驚いています。その場合、読み取りスレッドは、構築が完了していないオブジェクトを読み取る可能性があります。コンパイラはそのような状況を防ぎますか?

言及することを検討しましたが、違いはありません。new 演算子は、コンストラクターが完了するまで参照を返しません (したがって、フィールドへの割り当ては行われません)。これは、コンパイラーではなく、ランタイムによって保証されます。

ただし、言語/ランタイムは、他のスレッドが部分的に構築されたオブジェクトを認識できないことを実際に保証するものではありません。それは、コンストラクターの動作に依存します。

アップデート:

OPは、このページに役立つアイデアがあるかどうかも疑問に思います。彼らの最後のコード スニペットは、ダブル チェック ロックのインスタンスです。これは、何千人もの人々がどのように正しく行うかを考えずにお互いに推奨するアイデアの典型的な例です。問題は、SMP マシンが独自のメモリ キャッシュを持つ複数の CPU で構成されていることです。メモリが更新されるたびにキャッシュを同期する必要があるとしたら、複数の CPU を使用するメリットが台無しになります。そのため、ロックが解除されたとき、連動操作が発生したとき、またはvolatile変数がアクセスされたときに発生する「メモリバリア」でのみ同期します。

イベントの通常の順序は次のとおりです。

  • Coder がダブルチェック ロックを発見
  • Coder がメモリバリアを発見

これら 2 つのイベントの間に、多くの壊れたソフトウェアがリリースされます。

また、多くの人が(彼のように)連動操作を使用することで「ロックをなくす」ことができると信じています。しかし、実行時にはそれらはメモリ バリアであるため、すべての CPU が停止し、キャッシュが同期されます。OSカーネルへの呼び出しを行う必要がないという点でロックよりも優れていますが(それらは「ユーザーコード」のみです)、同期手法と同じくらいパフォーマンスを低下させる可能性があります.

要約: スレッド コードは、実際よりも約 1000 倍簡単に記述できます。

于 2009-03-16T21:26:29.293 に答える
5

ロックを使用する必要があります。そうしないm_PropNameと、既存の 2 つのインスタンスが別のスレッドで使用されている危険性があります。多くの場合、これは問題にはなりません。==ただし、代わりに使用できるようにしたい場合は.equals()、これが問題になります。まれな競合状態は、より良いバグではありません。それらはデバッグと再現が困難です。

コードで、2 つの異なるスレッドが同時にプロパティを取得する場合PropName(マルチコア CPU など)、同じデータを含むが同じオブジェクト インスタンスではない、プロパティの異なる新しいインスタンスを受け取ることができます。

不変オブジェクトの主な利点の 1 つは、==と同等であるため.equals()、よりパフォーマンスの高いオブジェクト==を比較に使用できることです。遅延初期化で同期しないと、この利点が失われる危険があります。

また、不変性も失います。オブジェクトは異なるオブジェクト (同じ値を含む) で 2 回初期化されるため、プロパティの値を既に取得しているスレッドが再度取得すると、2 回目は異なるオブジェクトを受け取る可能性があります。

于 2009-03-16T21:36:09.067 に答える
1

データが常にアクセスされるとは限らず、データを取得または保存するためにかなりの量のリソースが必要になる可能性がある場合、私はすべて遅延初期化に賛成です。

ここで重要な概念が忘れられていると思います。C# の設計概念に従って、インスタンス メンバーをデフォルトでスレッドセーフにしないでください。 デフォルトでは、静的メンバーのみをスレッドセーフにする必要があります。静的/グローバル データにアクセスする場合を除き、コードに余分なロックを追加しないでください。

あなたのコードが示すものから、遅延初期化はすべてインスタンス プロパティ内にあるため、ロックを追加しません。設計上、複数のスレッドが同時にアクセスすることを意図している場合は、先に進んでロックを追加してください。

ところで、コードを大幅に削減することはできないかもしれませんが、私は null-coalesce 演算子のファンです。代わりに、getter の body を次のようにすることもできます。 これにより、余分なものが取り除かれ、私の意見では、より簡潔で読みやすくなります。

m_PropName = m_PropName ?? new ...();
return m_PropName;


"if (m_PropName == null) ..."

于 2009-03-18T03:41:05.667 に答える
0

私は C# の専門家ではありませんが、私が知る限り、これは ReadOnlyCollection のインスタンスを 1 つだけ作成する必要がある場合にのみ問題を引き起こします。作成されたオブジェクトは常に同じであり、2 つ (またはそれ以上) のスレッドが新しいインスタンスを作成してもかまわないと言うので、ロックなしでこれを実行しても問題ないと思います。

後で奇妙なバグになる可能性のあることの 1 つは、インスタンスの等価性を比較する場合であり、同じではない場合があります。しかし、それを心に留めておけば (または単にそうしなければ)、他に問題はないと思います。

于 2009-03-16T21:29:32.563 に答える
0

残念ながら、ロックが必要です。適切にロックしないと、かなり微妙なバグがたくさんあります。困難な例については、この回答をご覧ください。

于 2009-03-16T22:02:56.447 に答える
0

フィールドが空白であるか、書き込まれる値または場合によっては同等の値をすでに保持している場合にのみフィールドが書き込まれる場合は、ロックなしで遅延初期化を安全に使用できます。2 つの変更可能なオブジェクトが同等ではないことに注意してください。変更可能なオブジェクトへの参照を保持するフィールドは、同じオブジェクトへの参照でのみ書き込むことができます(つまり、書き込みは効果がありません)。

状況に応じて、遅延初期化に使用できる 3 つの一般的なパターンがあります。

  1. 書き込む値の計算にコストがかかり、そのような労力を不必要に費やすことを避けたい場合は、ロックを使用してください。ダブルチェック ロック パターンは、メモリ モデルがサポートしているシステムに適しています。
  2. 不変の値を格納している場合は、必要に応じて計算し、格納するだけです。ストアを認識しない他のスレッドは冗長な計算を実行する可能性がありますが、既存の値をフィールドに書き込もうとするだけです。
  3. 安価に生成できるミュータブル クラス オブジェクトへの参照を格納する場合、必要と思われる場合は新しいオブジェクトを作成し、フィールドがまだ空白の場合は Interlocked.CompareExchange を使用して格納します。

スレッド内の最初のアクセス以外のアクセスでのロックを回避できる場合、レイジー リーダーをスレッド セーフにしても、重大なパフォーマンス コストは発生しないことに注意してください。可変クラスがスレッド セーフでないことはよくあることですが、不変であると主張するすべてのクラスは、リーダー アクションの任意の組み合わせに対して 100% スレッド セーフである必要があります。このようなスレッド セーフ要件を満たすことができないクラスは、不変であると主張すべきではありません。

于 2012-10-10T17:16:52.650 に答える
-1

これは間違いなく問題です。

次のシナリオを考えてみましょう: スレッド "A" がプロパティにアクセスし、コレクションが初期化されます。ローカル インスタンスをフィールド "m_PropName" に割り当てる前に、スレッド "B" はプロパティにアクセスしますが、完了する必要があります。スレッド "B" は現在 "m_PropName" に格納されているそのインスタンスへの参照を持っています... スレッド "A" が続行されるまで、その時点で "m_PropName" はそのスレッドのローカル インスタンスによって上書きされます。

現在、いくつかの問題があります。まず、所有しているオブジェクトが「m_PropName」が唯一のインスタンスであると考えているため、スレッド「B」はもはや正しいインスタンスを持っていませんが、スレッド「B」がスレッド「A」の前に完了したときに初期化されたインスタンスを漏らしました。もう 1 つは、スレッド "A" とスレッド "B" がインスタンスを取得した時点でコレクションが変更された場合です。次に、間違ったデータがあります。読み取り専用コレクションを内部的に監視または変更している場合はさらに悪化する可能性があります (もちろん、ReadOnlyCollection を使用することはできませんが、イベントを介して監視したり内部的に変更したりできる他の実装に置き換えた場合は可能ですが、外部ではありません)。

于 2009-03-16T21:48:50.207 に答える