[英]Thread safety for high-performance in-memory cache
我有一个静态内存缓存,只能写入一小时( 或更长 )一次, 并被许多线程以极高的速率读取 。 传统观点认为我遵循以下模式:
public static class MyCache
{
private static IDictionary<int, string> _cache;
private static ReaderWriterLockSlim _sharedLock;
static MyCache()
{
_cache = new Dictionary<int, string>();
_sharedLock = new ReaderWriterLockSlim();
}
public static string GetData(int key)
{
_sharedLock.EnterReadLock();
try
{
string returnValue;
_cache.TryGetValue(key, out returnValue);
return returnValue;
}
finally
{
_sharedLock.ExitReadLock();
}
}
public static void AddData(int key, string data)
{
_sharedLock.EnterWriteLock();
try
{
if (!_cache.ContainsKey(key))
_cache.Add(key, data);
}
finally
{
_sharedLock.ExitWriteLock();
}
}
}
作为微优化的练习,如何在共享读锁的相对费用中削减更多的滴答声? 写作的时间可能很昂贵,因为很少发生。 我需要尽可能快地进行读取。 我可以删除读锁(下面)并在此方案中保持线程安全吗? 或者我可以使用无锁版本吗? 我熟悉内存防护但不知道如何在这种情况下安全地应用它。
注意:我不依赖于任何一种模式,因此只要最终结果更快并且在C#4.x中,任何建议都是受欢迎的。*
public static class MyCache2
{
private static IDictionary<int, string> _cache;
private static object _fullLock;
static MyCache2()
{
_cache = new Dictionary<int, string>();
_fullLock = new object();
}
public static string GetData(int key)
{
//Note: There is no locking here... Is that ok?
string returnValue;
_cache.TryGetValue(key, out returnValue);
return returnValue;
}
public static void AddData(int key, string data)
{
lock (_fullLock)
{
if (!_cache.ContainsKey(key))
_cache.Add(key, data);
}
}
}
当只有线程只读取数据结构时,您不需要锁定。 因此,由于写入是如此罕见(并且,我假设,而不是并发),一个选项可能是制作字典的完整副本,对副本进行修改,然后用新的字典原子地交换旧字典:
public static class MyCache2
{
private static IDictionary<int, string> _cache;
static MyCache2()
{
_cache = new Dictionary<int, string>();
}
public static string GetData(int key)
{
string returnValue;
_cache.TryGetValue(key, out returnValue);
return returnValue;
}
public static void AddData(int key, string data)
{
IDictionary<int, string> clone = Clone(_cache);
if (!clone.ContainsKey(key))
clone.Add(key, data);
Interlocked.Exchange(ref _cache, clone);
}
}
我希望在这里免费锁定,并通过简单地不更改任何已发布的字典来实现线程安全。 我的意思是:当你需要添加数据时,创建一个完整的字典副本,并附加/更新/等副本 。 由于这是一小时一次,因此即使对于大数据也不应该成为问题。 然后,当您进行更改时,只需将旧字典中的引用交换到新字典(引用读/写保证是原子的)。
一个警告:任何需要在多个操作之间保持一致状态的代码应首先将字典捕获到变量中,即
var snapshot = someField;
// multiple reads on snapshot
这确保了所有相关逻辑都是使用相同版本的数据进行的,以避免在操作期间参考交换时出现混淆。
我还会在写入时 (而不是在阅读时)锁定,以确保不会对数据进行争吵。 还有无锁多写器方法(主要是Interlocked.CompareExchange,如果失败则重新应用),但我会先使用最简单的方法,而单个编写器就是这样。
替代选项:.net 1.x Hashtable(基本上是Dictionary,减去泛型)有一个有趣的线程故事; 读取是没有锁的线程安全 - 您只需要使用锁来确保最多一个编写器。
所以:您可以考虑使用非泛型Hashtable,不读取锁定,然后在写入期间锁定。
这是我有时甚至在.net 4.x应用程序中发现自己使用Hashtable的主要原因。
但是有一个问题 - 它会导致int键被存储和查询装箱。
这仅在添加数据时生成字典的副本。 锁用于添加,但如果您不打算从多个线程添加,则可以将其取出。 如果没有副本,则从原始字典中提取数据,否则在添加时使用副本。
为了防止副本在检查完并被视为非空后但在它能够检索值之前被取消,我添加了一个try catch,在那个罕见的事件中,它将从原始数据中提取数据,然后锁定但是再次如果有的话,这应该很少发生。
public static class MyCache2
{
private static IDictionary<int, string> _cache;
private static IDictionary<int, string> _cacheClone;
private static Object _lock;
static MyCache2()
{
_cache = new Dictionary<int, string>();
_lock = new Object();
}
public static string GetData(int key)
{
string returnValue;
if (_cacheClone == null)
{
_cache.TryGetValue(key, out returnValue);
}
else
{
try
{
_cacheClone.TryGetValue(key, out returnValue);
}
catch
{
lock (_lock)
{
_cache.TryGetValue(key, out returnValue);
}
}
}
return returnValue;
}
public static void AddData(int key, string data)
{
lock (_lock)
{
_cacheClone = Clone(_cache);
if (!_cache.ContainsKey(key))
_cache.Add(key, data);
_cacheClone = null;
}
}
}
您还可以查看无锁数据结构。 http://www.boyet.com/Articles/LockfreeStack.html就是一个很好的例子
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.