現在、C#で不変のAVLツリーをバケットとして内部的に使用するスレッドセーフな辞書を実装しています。私のアプリケーションのコンテキストでは、起動時にのみこの辞書にエントリを追加するため、値は大部分が読み込まれます(しかし、まだいくつかの書き込みがあります)。スレッドセーフな辞書実装でデータ競合が発生するのはなぜですか?
私は、次のように私のTryGetValue
とGetOrAdd
方法を構造化しました:あなたが見ることができるように
public sealed class FastReadThreadSafeDictionary<TKey, TValue> where TKey : IEquatable<TKey>
{
private readonly object _bucketContainerLock = new object();
private ImmutableBucketContainer<TKey, TValue> _bucketContainer;
public bool TryGetValue(TKey key, out TValue value)
{
var bucketContainer = _bucketContainer;
return bucketContainer.TryFind(key.GetHashCode(), key, out value);
}
public bool GetOrAdd(TKey key, Func<TValue> createValue, out TValue value)
{
createValue.MustNotBeNull(nameof(createValue));
var hashCode = key.GetHashCode();
lock (_bucketContainerLock)
{
ImmutableBucketContainer<TKey, TValue> newBucketContainer;
if (_bucketContainer.GetOrAdd(hashCode, key, createValue, out value, out newBucketContainer) == false)
return false;
_bucketContainer = newBucketContainer;
return true;
}
}
// Other members omitted for sake of brevity
}
が、私はreference assignment in .NET runtimes is an atomic operation by designのでTryGetValue
にロックを使用しないでください。フィールド_bucketContainer
の参照をローカル変数にコピーすることにより、インスタンスに安全にアクセスすることができると確信しています。 GetOrAdd
で私は秘密の_bucketContainer
にアクセスするためにロックを使用するので、値が2回作成されないようにすることができます(つまり、2つ以上のスレッドが値を追加しようとしている場合、ロックの)。
[DataRaceTestMethod]
public void ReadWhileAdd()
{
var testTarget = new FastReadThreadSafeDictionary<int, object>();
var writeThread = new Thread(() =>
{
for (var i = 5; i < 10; i++)
{
testTarget.GetOrAdd(i,() => new object());
Thread.Sleep(0);
}
});
var readThread = new Thread(() =>
{
object value;
testTarget.TryGetValue(5, out value);
Thread.Sleep(0);
testTarget.TryGetValue(7, out value);
Thread.Sleep(10);
testTarget.TryGetValue(9, out value);
});
readThread.Start();
writeThread.Start();
readThread.Join();
writeThread.Join();
}
:私はテストの同時実行のための
Microsoft Chessを使用して、私は、古いものと新しいバケットコンテナを交換したときに私のテストの一つに、MCUT(マイクロソフト同時実行ユニットテスト)が
GetOrAdd
でのデータ競合を報告し
23>試験結果:データ競合 23> ReadWhileAdd()(コンテキスト=、をTestType = MChess):GetOrAddで【データ競合]実測データレース:FastR
MCUTは、次のメッセージを報告しますeadThreadSafeDictionary.cs(68)
はGetOrAdd
です。
私の実際の質問は:_bucketContainer = newBucketContainer
は競合状態ですか?現在実行中のスレッドTryGetValue
は、常に_bucketContainer
フィールドのコピーを作成するので、コピーが行われた直後に検索された値が_bucketContainer
に追加される可能性がある点を除いては、更新で悩まされるべきではありません。データ競争)。 GetOrAdd
には、同時アクセスを防ぐための明示的なロックがあります。これはチェスのバグでしょうか、何か非常に明白なものがありませんか?
あなたは揮発性の読み取りを使用していないとあなたが新しい状態を構築し、フィールドに割り当てるとの間のメモリバリアが必要になる場合があります。 .net 2.0メモリモデルについてはわかりませんが、これらの両方がECMAメモリモデルに必要だと思います。 – CodesInChaos
@CodesInChaos私は 'TryGetValue'に' Volatile.Read'コールを追加しました。これはテストをパスします(ありがとう!)。それでも、なぜこれが問題であるのか分かりません。なぜなら、 'Volatile.Read'は値がメモリから読み出され、キャッシュするCPUレジスタから読み出されることを保証しないからです。バケットコンテナ自体は不変なので、なぜこれが競合状態になるのでしょうか? 'TryGetValue'は場合によっては古いバージョンを使用するかもしれませんが、全体的にパフォーマンスは' Volatile.Read'よりもかなり良いはずです。 – feO2x
私はこの状況で競合状態になる理由を理解していません。(C#言語では、参照の読み書きがアトミックであることが保証されています)。無効な値が得られる可能性があります(たとえば、参照がレジスタで更新されていてもメインメモリにコピーされていないなど)が、フィールドを使用しているコンテキストで競合状態になっているとは限りません。 –