スレッド セーフ オブジェクトの処理 - 例: キャッシュ オブジェクト
MSDN の状態:
スレッド セーフ: このタイプはスレッド セーフです。
マルチスレッド環境でのキーによる設定に関する意味を理解しています。
しかし、マルチスレッド環境でキーを取得するのはどうでしょうか?
200
スレッドが値を読み取りたい場合、199
他のスレッドはブロックまたは待機していますか?
助けてくれてありがとう。
スレッド セーフ オブジェクトの処理 - 例: キャッシュ オブジェクト
MSDN の状態:
スレッド セーフ: このタイプはスレッド セーフです。
マルチスレッド環境でのキーによる設定に関する意味を理解しています。
しかし、マルチスレッド環境でキーを取得するのはどうでしょうか?
200
スレッドが値を読み取りたい場合、199
他のスレッドはブロックまたは待機していますか?
助けてくれてありがとう。
スレッドセーフを提供するにはさまざまな方法があり、どちらを使用するかは指定されていませんが、読み取りをロックする必要なく発生する可能性があります。簡単な答えは、構造化されているため、読み取りにロックは必要ありません。長い答え:
「すべてのメソッドはスレッドセーフである」という約束は、これらのメソッドが同時に呼び出された場合、オブジェクトを破損したり、誤った結果を返したりしないということです。オブジェクトを考えてみましょうList<char>
。
空のList<char>
呼び出しlist
があり、3 つのスレッドが同時に以下を呼び出すとします。
char c = list[0]
.list.Add('a');
list.Add('b');
4 つの正しい結果が考えられます。
c
が含まれています'a'
。list
が含まれています{'a', 'b'}
。list.Count
今戻ってき2
ます。c
が含まれています'b'
。list
が含まれています{'b', 'a'}
。list.Count
今戻ってき2
ます。ArgumentOutOfRangeException
は最初のスレッドで発生します。list
が含まれています{'a', 'b'}
。list.Count
今戻ってき2
ます。ArgumentOutOfRangeException
は最初のスレッドで発生します。list
が含まれています{'b', 'a'}
。list.Count
今戻ってき2
ます。リストはリストであるため、どちらか一方のアイテムをもう一方の前に挿入する必要があるため、これらはすべて正しいです。また、最初に返された値が によって返されるようにする必要がありますlist[0]
。また、スレッド 1 の読み取りを、この挿入の後または前に発生したものとして処理する必要があるため、その最初の要素を挿入するかc
、ArgumentOutOfRangeException
スローします (空の要素の最初の項目を見て、スレッド セーフの欠陥ではありません)。スレッドの問題に関係なく、リストは範囲外です。
ただしList<char>
、スレッドセーフではないため、次のことが発生する可能性があります。
list
が含まれています{'a'}
。list.Count
1 を返しますc
。'b'
list
が含まれています{'b'}
。list.Count
1 を返しますc
。'a'
list
が含まれています{'a'}
。list.Count
2 を返します。c
含む'b'
list
が含まれています{'b'}
。list.Count
2 を返します。c
含む'a'
IndexOutOfRangeException
. ArgumentOutOfRangeException
(最初のスレッドが発生するのに許容される動作ではないことに注意してください)。list
例外が発生します。List<char>
言い換えれば、list
プログラマーが考えていなかった状態になるまで全体を完全に台無しにし、すべての賭けがオフになっています。
では、スレッド セーフなコレクションを構築するにはどうすればよいでしょうか。
簡単な方法は、状態を変更するか、状態に依存するすべてのメソッドをロックすることです (これらすべてになる傾向があります)。静的メソッドの場合、静的フィールドをロックします。たとえば、インスタンス フィールドをロックするメソッドです (静的フィールドをロックできますが、別のオブジェクトに対する操作は互いにブロックされます)。パブリック メソッドが別のパブリック メソッドを呼び出した場合、ロックが 1 回だけ行われるようにします (おそらく、ロックしないプライベート ワーカー メソッドを呼び出すパブリック メソッドをそれぞれ持つことによって)。一部の分析により、すべての操作を互いに独立した 2 つ以上のグループに分割し、グループごとに個別のロックを設定できることが示される可能性がありますが、可能性は低いです。
プラスは単純さです。マイナスは多くのロックがかかり、相互に安全な操作が相互にブロックされる可能性があります。
もう 1 つは、共有排他ロックです (実際には正反対のシナリオを含む、単一ライターの複数リーダー以外のシナリオがあるため、.NET は誤った名前ReaderWriterLock
と- 誤った名前で提供されます)。ReaderWriterLockSlim
読み取り専用操作 (内部から見た場合 - メモ化を使用する読み取り専用操作は、内部キャッシュを更新する可能性があるため、実際には読み取り専用操作ではありません) は同時に安全に実行できることは事実ですが、操作は唯一の操作である必要があり、共有排他ロックによりそれが保証されます。
もう 1 つは、ストライプ ロックによるものです。これが、現在の の実装のConcurrentDictionary
しくみです。いくつかのロックが作成され、各書き込み操作にロックが割り当てられます。他の書き込みが同時に発生する可能性がありますが、互いに害を及ぼす書き込みは同じロックを取得しようとします。一部の操作では、すべてのロックを取得する必要があります。
もう 1 つは、ロックフリー同期によるものです (使用されるプリミティブの機能が、ある意味ではロックに似ているが別の点では似ていないため、「低ロック」と呼ばれることもあります)。https://stackoverflow.com/a/3871198/400547は、単純なロックフリー キューを示しており、ConcurrentQueue
別のロックフリー テクニックを再び使用しています (.NET では、最後に Mono を見たときにその例に近かった)。
ロックフリーのアプローチは自動的に最良の方法のように聞こえるかもしれませんが、常にそうであるとは限りません。https://github.com/hackcraft/Ariadne/blob/master/Collections/ThreadSafeDictionary.csにある私自身のロックフリー辞書は、場合によっては のストライプ ロックよりも優れていますConcurrentDictionary
が、他の多くの場合はそうではありません。なんで?まあ、最初ConcurrentDictionary
はほぼ間違いなく、私がまだ私に与えることができたよりも多くの時間をチューニングに費やしてきました! 真剣に、ロックのないアプローチが安全であることを確認するには、他のコードとまったく同じようにサイクルとメモリを消費するロジックが必要です。ロックのコストを上回る場合もあれば、そうでない場合もあります。(編集:現在、私はConcurrentDictionary
幅広いケースで打ち負かすようになりましたが、その後、背後にいる人々ConcurrentDictioanry
良いので、次のアップデートでより多くのケースでバランスが戻ってくるかもしれません)。
最後に、いくつかの構造は、それ自体で一部の操作に対してスレッドセーフであるという単純な事実があります。int[]
たとえば、複数のスレッドが読み取りと書き込みを行っている場合myIntArray[23]
、さまざまな結果が考えられますが、それらのいずれも構造を破損せず、すべての読み取りで初期状態または次のいずれかが表示される限り、スレッドセーフです。スレッドの 1 つが書き込んでいた値。ただし、メモリ バリアを使用して、CPU キャッシュが古いために最後のライターが終了した後もリーダー スレッドがまだ初期状態を認識していないことを確認したい場合があります。
ここで、真であることは真でint[]
もありますobject[]
(ただし、実行中の CPU がアトミックに処理するものよりも大きな値型の配列には真ではないことに注意してください)。int[]
とobject[]
は一部の辞書ベースのクラスで内部的に使用されているためHashtable
(ここで特に興味深いのCache
は例です)、単一の書き込みスレッドと複数の読み取りスレッド (複数の書き込みスレッドではなく、複数の読み取りスレッド) に対して構造が自然にスレッドセーフになるようにそれらを構築することが可能です。ときどきライターが内部ストアのサイズを変更する必要があり、スレッドセーフにするのが難しいからです)。これはHashtable
(IIRC の場合ですDictionary<TKey, TValue>
。TKey
TValue
TKey
、しかし動作は保証されておらず、およびの他のタイプには当てはまりませんTValue
)。
複数のリーダー/単一のライターに対して自然にスレッドセーフな構造を作成したら、複数のリーダー/複数のライターに対してスレッドセーフにすることは、書き込みをロックすることです。
最終結果は、スレッド セーフのすべての利点を得ることができますが、そのコストはリーダーにはまったく影響しません。
スレッドセーフは、スレッドセーフを実現するための特定の戦略(ロックなど)を意味するものではありません。複数のスレッドがオブジェクトと相互作用している場合、それはそのコントラクトされた動作を生成し続けることを単に示しています-コントラクトは各パブリックまたは保護されたメンバーの文書化された動作です。
これ以上のドキュメントがない場合は、このスレッドセーフがどのように達成されるかに関する実装の詳細であり、変更される可能性があります。
現在、すべてのアクセスで排他ロックを取得する可能性があります。または、ロックフリーの手法を使用する場合もあります。どちらの方法でも文書化されていませんが、読み取り操作中に排他ロックが取得された場合は非常に驚きます。
スレッド セーフは読み取りには適用されません。キャッシュは、レコードの削除/挿入に対してスレッドセーフです。複数のスレッドが同時に値を読み取ることができます。スレッドが値を読み取っている間に値が削除された場合、削除後に取得した値はキャッシュから null 値を取得します (キャッシュ ミス)。
同時書き込みの場合、 .NET4には非常にうまく機能するConcurrentQueueコレクションがあります。私はそれを何度も使用しましたが、まだ問題はありません。読み取りに関しては、並列処理にリストコレクションを使用しており、正しい値を返します。以下に、私がそれをどのように使用したかについての非常に基本的な例を参照してください。
List<string> collection = new List<string>(); // Empty list in this example
ConcurrentQueue<string> concurrent = new ConcurrentQueue<string>();
Parallel.ForEach(collection, new ParallelOptions {MaxDegreeOfParallelism = Environment.ProcessorCount}, item =>
{
concurrent.Enqueue(item);
});
ただし、質問に答えるために、コレクションは同時に読み取るときに他のスレッドをロックしません。ただし、(同時に)キューに書き込みを行っていた場合、一部のアイテムが追い出されて挿入されない可能性が高くなります。'lock'を使用してそれを回避できますが、これにより他のスレッドが一時的にブロックされます。
私が理解している限り、他のスレッドは、オブジェクトへの書き込み中にブロックされます。
オブジェクトを読み取るときに他のスレッドをブロックすることは意味がありません-読み取り操作はオブジェクトの状態を変更しないためです。