高性能内存缓存的线程安全性
本文关键字:线程 安全性 缓存 内存 高性能 | 更新日期: 2023-09-27 18:15:07
我有一个静态内存缓存,每小时只写入一次(或更长),并且许多线程以极高的速率读取。传统智慧建议我遵循如下模式:
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();
}
}
}
作为一个微优化的练习,我如何在共享read锁的相对开销中减少更多的滴答?写入的时间可能很昂贵,因为这种情况很少发生。我需要尽可能快地阅读。在这种情况下,我可以删除read锁(见下文)并保持线程安全吗?或者是否有可以使用的无锁版本?我熟悉内存保护,但不知道如何在这个实例中安全地应用它。
注意:我不依赖于任何模式,所以任何建议都是欢迎的,只要最终结果更快,在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
这确保所有相关逻辑都使用数据的相同版本,以避免在操作期间引用交换时产生混淆。
我还会在写时(而不是在读时)使用锁,以确保不会在数据上发生争吵。也有无锁的多写器方法(主要是互锁)。CompareExchange,并在失败时重新应用),但我将首先使用最简单的方法,单个写入器正是如此。可选:.netx Hashtable(本质上是Dictionary,减去泛型)有一个有趣的线程故事;在没有锁的情况下,读是线程安全的——你只需要使用锁来确保最多有一个写入器。
所以:你可以考虑使用非泛型哈希表,在读时不加锁,然后在写时加锁。
这是我发现自己有时仍在使用哈希表的主要原因,即使是在。net 4中。x应用程序。
有一个问题——它会导致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是一个很好的例子