我目前正在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
}
正如您所看到的,我不会在TryGetValue
中使用锁定,因为reference assignment in .NET runtimes is an atomic operation by design。通过将字段_bucketContainer
的引用复制到本地变量,我确信我可以安全地访问该实例,因为它是不可变的。在GetOrAdd
中,我使用锁来访问私有_bucketContainer
,因此我可以确保不会创建两次值(即,如果两个或多个线程正在尝试添加值,则只有一个可以实际创建一个新的ImmutableBucketContainer
,由于锁定而具有附加值。
我使用Microsoft Chess来测试并发性,在我的一个测试中,MCUT(Microsoft并发单元测试)在我用新旧桶交换新的桶容器时报告GetOrAdd
中的数据竞争之一:
[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();
}
MCUT报告以下消息:
23&GT;测试结果:DataRace 23 GT; ReadWhileAdd()(Context =,TestType = MChess):[DataRace]在GetOrAdd找到数据竞争:FastReadThreadSafeDictionary.cs(68)
_bucketContainer = newBucketContainer;
中的作业GetOrAdd
。
我的实际问题是:为什么作业_bucketContainer = newBucketContainer
是竞争条件?当前正在执行的广告TryGetValue
总是复制_bucketContainer
字段,因此不应该对更新感到困扰(除了在复制发生后可能会将搜索到的值添加到_bucketContainer
,但这与数据竞争无关)。在GetOrAdd
中,有一个显式锁定来阻止并发访问。这是国际象棋中的一个错误还是我错过了一些非常明显的错误?
答案 0 :(得分:0)
正如@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或Itanium等其他体系结构可能不是这样。这就是为什么必须使用内存屏障与其他线程同步读访问,内存屏障在Volatile.Read
内部执行(注意lock
语句内部也使用内存屏障)。 Joseph Albahari在这里写了一篇全面的教程:http://www.albahari.com/threading/part4.aspx