我目前正在编写一些极其性能关键的代码,需要一种方法来从键值存储中检索值而不使用锁。
我尝试过使用ConcurrentDictionary,但在这种情况下,这并不能满足我的需求。
所以我在这里的内容类似于ConcurrentDictionary中的GetOrAdd方法,但是我需要它超快(没有锁)并且仍然是线程安全的:)
这里应该注意,我们假设我们主要是检索现有值,很少添加新值。还假设这个列表不会很大。
我不是线程专家,所以如果有人能对我的想法发表评论会很好。
public class Registry<TKey, TValue>
{
private Dictionary<TKey, TValue> dictionary = new Dictionary<TKey, TValue>();
public TValue GetOrAdd(TKey key, Func<TKey, TValue> valueFactory)
{
TValue value;
if (!dictionary.TryGetValue(key, out value))
{
var snapshot = new Dictionary<TKey, TValue>(dictionary);
if (!snapshot.TryGetValue(key, out value))
{
value = valueFactory(key);
snapshot.Add(key, value);
dictionary = snapshot;
}
}
return value;
}
}
这里的“技巧”是如果我们确实需要为它添加新值,则创建实际字典的快照。最后,我们交换引用,以便字典变量现在指向快照。 请记住,如果我在这里和那里放松一两个更新,我真的不在乎。 我需要的是真正快速检索现有值。
我对交换引用的代码有点不确定。
dictionary = snapshot;
如果另一个线程在交换引用的同时尝试访问字典变量,会发生什么。这甚至是一个问题吗?
问候
Bernhard Richter
答案 0 :(得分:4)
你的第一次拍摄几乎是正确的。在地址以原子方式更新的意义上,交换引用是线程安全的。但是,当您想要更新字典时仍然需要锁定,因为您不想丢失任何并发更改。实现这一目标的唯一方法是采取某种锁定。
如果不这样做,你有时会使用较旧版本的字典,尽管引用已在其间更新,然后你的线程会将引用与包含旧数据的更新字典交换。
MS Threading手册还提到,当您很少更新字典时,ConcurrentDictionary不能很好地扩展。在这种情况下,经典字典仍然更好。
我不知道你遇到了哪个问题域,但是改变你正在处理的数据结构可能会给你提供比优化并发字典访问更多的性能。字典非常快,但CPU数据缓存不友好。如果你想要更快,你可能需要摆脱字典,或者你需要不同的数据结构来获得更多的内存缓存,这更加缓存友好。
public class Registry<TKey, TValue>
{
private Dictionary<TKey, TValue> dictionary = new Dictionary<TKey, TValue>();
private object Lock = new object();
public TValue GetOrAdd(TKey key, Func<TKey, TValue> valueFactory)
{
TValue value;
if (!dictionary.TryGetValue(key, out value))
{
lock(Lock)
{
var snapshot = new Dictionary<TKey, TValue>(dictionary);
if (!snapshot.TryGetValue(key, out value))
{
value = valueFactory(key);
snapshot.Add(key, value);
dictionary = snapshot;
}
}
}
return value;
}
}
答案 1 :(得分:1)
这不对!在创建快照之前,字典的构造函数将遍历整个字典。这可能导致快照状态不正确。
如果ConcurrentDictionary对您没有帮助,您可以尝试使用不可变数据结构,如前缀树(又名基数树或特里)。这些是线程安全的。
答案 2 :(得分:1)
由于您的字典大小不大而且您不经常更新它,您可以使用双缓冲方法:
创建两个完全相同的词典,并从第一个词典中读取。将更新应用于第二个字典并交换引用,以便您现在从第二个字典中读取。然后返回并将相同的更新应用于第一个字典。
将下一个更新应用于第一个字典,交换引用,然后更新第二个字典。等等,每次更新时都会在字典之间翻转。