C# 为什么我的线程安全字典实现会产生数据竞争?
我目前正在C#中实现一个线程安全字典,它在内部使用不可变的AVL树作为存储桶。其思想是提供无锁的快速读取访问,因为在我的应用程序上下文中,我们仅在启动时向该字典添加条目,之后,大部分值都被读取(但仍有少量写入) 我以以下方式构造了我的C# 为什么我的线程安全字典实现会产生数据竞争?,c#,.net,multithreading,dictionary,concurrency,C#,.net,Multithreading,Dictionary,Concurrency,我目前正在C#中实现一个线程安全字典,它在内部使用不可变的AVL树作为存储桶。其思想是提供无锁的快速读取访问,因为在我的应用程序上下文中,我们仅在启动时向该字典添加条目,之后,大部分值都被读取(但仍有少量写入) 我以以下方式构造了我的TryGetValue和GetOrAdd方法: 公共密封类FastReadThreadSafeDictionary,其中TKey:IEquatable { 私有只读对象_bucketContainerLock=新对象(); 私有不可变bucketContainer_
TryGetValue
和GetOrAdd
方法:
公共密封类FastReadThreadSafeDictionary,其中TKey:IEquatable
{
私有只读对象_bucketContainerLock=新对象();
私有不可变bucketContainer_bucketContainer;
公共bool TryGetValue(TKey键,out TValue值)
{
var bucketContainer=_bucketContainer;
返回bucketContainer.TryFind(key.GetHashCode(),key,out值);
}
公共bool GetOrAdd(TKey键、Func createValue、out TValue值)
{
MustNotBeNull(nameof(createValue));
var hashCode=key.GetHashCode();
锁(_bucketContainerLock)
{
不可变BucketContainer newBucketContainer;
if(_bucketContainer.GetOrAdd(hashCode、key、createValue、out value、out newBucketContainer)==false)
返回false;
_bucketContainer=newBucketContainer;
返回true;
}
}
//为简洁起见,省略了其他成员
}
如您所见,我没有在TryGetValue
中使用锁,因为。通过将字段\u bucketContainer
的引用复制到一个局部变量,我确信我可以安全地访问该实例,因为它是不可变的。在GetOrAdd
中,我使用一个锁来访问私有\u bucketContainer
,这样我就可以确保一个值不会被创建两次(即,如果两个或多个线程试图添加一个值,则只有一个线程可以实际创建一个新的ImmutableBucketContainer
,因为有了锁,所以会添加值)
我用于测试并发性,在我的一个测试中,当我将新的bucket容器与旧的bucket容器交换时,MCUT(Microsoft并发单元测试)在GetOrAdd
中报告数据竞争:
[DataRaceTestMethod]
公共无效readwhiledd()
{
var testTarget=新的FastReadThreadSafeDictionary();
var writeThread=新线程(()=>
{
对于(变量i=5;i<10;i++)
{
GetOrAdd(i,()=>newobject());
睡眠(0);
}
});
var readThread=新线程(()=>
{
目标价值;
testTarget.TryGetValue(5,输出值);
睡眠(0);
testTarget.TryGetValue(7,输出值);
睡眠(10);
testTarget.TryGetValue(9,输出值);
});
readThread.Start();
writeThread.Start();
readThread.Join();
writeThread.Join();
}
MCUT报告以下消息:
23>测试结果:DataRace
23>ReadWhileAdd()(Context=,TestType=MChess):[DataRace]在GetOrAdd:FastReadThreadSafeDictionary.cs(68)找到数据竞争
这是赋值\u bucketContainer=newBucketContainer代码>在GetOrAdd
中
我的实际问题是:为什么赋值\u bucketContainer=newBucketContainer
是竞争条件?当前执行TryGetValue
的线程总是复制\u bucketContainer
字段,因此不应该为更新而烦恼(除了在复制发生后,搜索的值可能会添加到\u bucketContainer
,但这与数据竞争无关)。在GetOrAdd
中,有一个明确的锁来防止并发访问。这是国际象棋中的一个bug还是我遗漏了一些非常明显的东西?正如@CodesInChaos在问题的评论中提到的,我遗漏了TryGetValue
中的一个易变读取。该方法现在看起来如下:
public bool TryGetValue(TypeKey typeKey, out TValue value)
{
var bucketContainer = Volatile.Read(ref _bucketContainer);
return bucketContainer.TryFind(typeKey, out value);
}
此易失性读取是必需的,因为访问此字典的不同线程可能会缓存数据并重新排序彼此独立的指令,这可能会导致数据竞争。此外,运行代码的CPU体系结构也很重要,例如,x86和x64处理器默认执行易失性读取,而这可能不会对于ARM或安腾等其他体系结构来说,情况并非如此。这就是为什么读取访问必须使用内存屏障与其他线程同步,内存屏障在Volatile中内部执行。读取
(请注意,lock
语句也在内部使用内存屏障).Joseph Albahari在此编写了一个全面的教程:您没有使用volatile read,在构造新状态和将其分配给字段之间可能需要一个内存屏障。不确定.net 2.0内存模型,但我认为这两个都是ECMA内存模型中必需的。@CodesInChaos我添加了一个volatile。阅读
调用TryGetValue
即可通过测试(谢谢!)但是,我不明白为什么这是一个问题,因为Volatile.Read
只是确保从内存中读取值,而不是从可能缓存它的CPU寄存器中读取值。由于bucket容器本身是不可变的,为什么这会导致争用条件?TryGetValue
在某些情况下可能只是使用它的旧版本,但总体而言l、 性能应该比使用Volatile要好得多。请阅读
。我也不明白您为什么要这样做