例外は、反復の途中でコレクションが変更された結果である可能性がありますIEnumerator
。スレッドセーフを維持するために使用できるテクニックはほとんどありません。難しい順に紹介します。
すべてをロック
これは、スレッドセーフなデータ構造にアクセスするための最も簡単で簡単な方法です。このパターンは、読み取り操作と書き込み操作の数が等しく一致する場合にうまく機能します。
LinkedList<object> collection = new LinkedList<object>();
void Write()
{
lock (collection)
{
collection.AddLast(GetSomeObject());
}
}
void Read()
{
lock (collection)
{
foreach (object item in collection)
{
DoSomething(item);
}
}
}
コピー読み取りパターン
これは少し複雑なパターンです。データ構造を読み取る前に、データ構造のコピーが作成されることに気付くでしょう。このパターンは、読み取り操作の数が書き込みの数に比べて少なく、コピーのペナルティが比較的小さい場合にうまく機能します。
LinkedList<object> collection = new LinkedList<object>();
void Write()
{
lock (collection)
{
collection.AddLast(GetSomeObject());
}
}
void Read()
{
LinkedList<object> copy;
lock (collection)
{
copy = new LinkedList<object>(collection);
}
foreach (object item in copy)
{
DoSomething(item);
}
}
コピー - 変更 - スワップ パターン
最後に、最も複雑でエラーが発生しやすいパターンを示します。自分が何をしているのかを本当に理解していない限り、このパターンを使用することは実際にはお勧めしません。以下の内容から逸脱すると、問題が発生する可能性があります。これを台無しにするのは簡単です。実際、私は過去にうっかりこれを台無しにしてしまいました。すべての変更の前に、データ構造のコピーが作成されていることがわかります。次にコピーが変更され、最後に元の参照が新しいインスタンスと交換されます。基本的に、私たちは常にcollection
不変であるかのように扱っています。このパターンは、書き込み操作の数が読み取りの数に比べて少なく、コピーのペナルティが比較的小さい場合にうまく機能します。
object lockobj = new object();
volatile LinkedList<object> collection = new LinkedList<object>();
void Write()
{
lock (lockobj)
{
var copy = new LinkedList<object>(collection);
copy.AddLast(GetSomeObject());
collection = copy;
}
}
void Read()
{
LinkedList<object> local = collection;
foreach (object item in local)
{
DoSomething(item);
}
}
アップデート:
そこで、コメント セクションで次の 2 つの質問をしました。
- なぜ書き込み側では
lock(lockobj)
なく?lock(collection)
- なぜ
local = collection
読み取り側で?
最初の質問に関しては、C# コンパイラがlock
.
void Write()
{
bool acquired = false;
object temp = lockobj;
try
{
Monitor.Enter(temp, ref acquired);
var copy = new LinkedList<object>(collection);
copy.AddLast(GetSomeObject());
collection = copy;
}
finally
{
if (acquired) Monitor.Exit(temp);
}
}
collection
これで、ロック式として使用した場合に何がうまくいかないかを簡単に確認できることを願っています。
- スレッド A が実行され
object temp = collection
ます。
- スレッド B が実行され
collection = copy
ます。
- スレッド C が実行され
object temp = collection
ます。
- スレッド A は、元の参照でロックを取得します。
- スレッド C は、新しい参照でロックを取得します。
明らかにこれは悲惨なことです!クリティカル セクションが複数回入力されるため、書き込みが失われます。
さて、2 番目の質問は少しトリッキーでした。上記で投稿したコードで必ずしもこれを行う必要はありません。でも、それはcollection
一度しか使っていないからです。次のコードを考えてみましょう。
void Read()
{
object x = collection.Last;
// The collection may get swapped out right here.
object y = collection.Last;
if (x != y)
{
Console.WriteLine("It could happen!");
}
}
ここでの問題はcollection
、いつでも交換できることです。これは、見つけるのが非常に難しいバグです。これが、このパターンを実行するときに常に読み取り側でローカル参照を抽出する理由です。これにより、各読み取り操作で同じコレクションを使用していることを確認できます。
繰り返しますが、このような問題は非常に微妙なので、本当に必要でない限り、このパターンを使用することはお勧めしません。