使用 ReaderWriterLockSlim C# 缓存数据的字典
Dictionary of cached data with ReaderWriterLockSlim C#
我想要一些关于我可以在哪里改进或更改当前代码设计的建议。
有一个已设置的管理器 class,它有一个由不同线程调用的计算方法。每个线程都有一个需要计算的资源。这些资源可能属于不同的公司。
对于每一个公司,我们都想缓存一些数据,但是在调用Calculate方法时,只能通过资源获取公司数据。
所以我目前的想法是在 Manager class 中有一个 Dictionary,以 companyResourceTag 作为 Key。
调用 Calculate 时,将确定 companyResourceTag 并调用 CheckCachedData 方法。
private void CheckCachedData(int companyResourceTag)
{
_ReaderWriterLock.EnterUpgradeableReadLock();
try
{
if (!_CompanyCachedData.ContainsKey(companyResourceTag))
{
Elements elements = ElementService.GetAllElements();
DataElements dataElements = ElementService.GetAllDataElements();
CalendarGroupings calendarGroupings = CalendarService.GetAllCalendarGroupings();
CachedDataContainer cachedItem = new CachedDataContainer(elements, dataElements, calendarGroupings);
_ReaderWriterLock.EnterWriteLock();
try
{
_CompanyCachedData.Add(companyResourceTag, cachedItem);
}
finally
{
_ReaderWriterLock.ExitWriteLock();
}
}
}
finally
{
_ReaderWriterLock.ExitUpgradeableReadLock();
}
}
如果以前没有来自该公司的资源,则必须通过服务获取该公司的数据。基础表不会经常更改,我们可以假设在所有资源的计算 运行 期间,表将保持不变。但是,获取这些数据非常耗时。因此需要缓存。
可能有 100 家不同的公司和 30000 多个资源需要计算。还有一些其他地方(每个资源)从中读取此缓存数据,例如:
_ReaderWriterLock.EnterReadLock();
try
{
_CompanyCachedData.TryGetValue(companyResourceTag, out cachedDataContainer);
}
finally
{
_ReaderWriterLock.ExitReadLock();
}
//Do something with cachedDataContainer
由于 Eric Lathrop 在这里的评论,我没有尝试使代码更优雅:
Possible problem
我没有使用 ConcurrentDictionary,因为这里提到的问题:Possible problem with ConcurrentDictionary
我不确定普通锁是否会比 ReaderWriterLockSlim 更好,但我喜欢这样的想法,即一次可以有多个 reader,并且我可以升级锁。目前我也更关心正确性而不是速度。
我是否正确使用了 RWLS?
您是否同意我目前对 UpgradeableReadLock 的使用?
你同意我不使用 ConcurrentDictionary 的选择吗?
缺席 a good, minimal, complete code example 无法肯定地说明代码。但是,您发布的代码在基本正确性方面似乎没问题。
但是……
Would you agree on my current use of the UpgradeableReadLock?
我通常希望看到检索和缓存存储是同一操作的一部分。 IE。有一个主要负责检索值的方法,如果找不到,则以缓慢的方式获取值,将其存储在缓存中,然后 return.
将缓存检索和原始提取分成两个单独的方法似乎很奇怪并且可能有错误(但是,如果没有好的代码示例,我不能肯定地说)。
(注意:上面我只是指调用者看到的 API。当然,实际的缓存实现会将实际的数据获取分解为一个单独的方法。只是我希望看到由用于从缓存中检索值的相同方法调用的方法,这样您只需在值实际上已经在缓存中的情况下获取读锁一次).
Would you agree on my choice of not using ConcurrentDictionary?
同样,缺乏更多的上下文很难说。我倾向于尽量避免使用 ConcurrentDictionary
,因为与传统的 Dictionary<TKey, TValue>
class 相比,它具有更难使用的语义,至少在利用其特定于并发的功能时是这样。
但在您的情况下,我认为那些特定于并发的功能可能正是您正在寻找的。特别是,TValue GetOrAdd(TKey key, Func<TKey, TValue> valueFactory)
方法实现了缓存通常需要的语义:检索现有值,或者如果键不存在则用新值填充字典。
另一方面,该方法有一个重要警告:如果从多个线程同时调用该方法,则 valueFactory
委托可能会被调用多次。只有一个结果将实际用于填充字典,它可能会或可能不会影响本地性能,但它显然会影响您从中获取数据的服务器的整体负载。
如果这是一个问题,那么 ReaderWriterLockSlim
可能是更好的选择,因为它可以让您确保只有一个作者。
在第三方面,我会在此处记录我之前关于相对性能的评论。也就是说,尚不清楚 ReaderWriterLockSlim
的潜在性能优势在这里是否真的很重要。从理论上讲,与使用 Monitor
class (即 lock
语句)相比,已经填充的情况应该有更少的开销,但我认为你必须处理对于真正发挥作用的争论非常激烈。
从您的示例中甚至不清楚 companyResourceTag
如何映射到缓存键的给定数据组,不要介意您希望看到代码从缓存中检索已获取数据的频率(假设假定获取数据的成本,我认为缓存未命中的同步成本无关紧要)。因此,问题中缺少很多重要的细节,需要准确的答案。
我想如果存在 非常 高水平的竞争,并且未命中 非常 罕见,ReaderWriterLockSlim
可能 会给您带来可衡量的性能提升。但除此之外,我希望 lock
提供具有完全可接受的性能的更简单的代码。
当然,这纯属猜测。同样,如果没有完整的代码示例,就无法确定哪种同步机制最适合您的场景。但是考虑到获取数据的假设成本,管理自己的同步似乎确实比使用 ConcurrentDictionary<TKey, TValue>
更好,因为后者没有提供避免多次获取数据的直接方法。
您是否应该使用 ReaderWriterLockSlim
而不是更简单的 lock
方法,甚至更具推测性。 可能 前者更好,但由于缺乏更多信息,很可能不是。
我想要一些关于我可以在哪里改进或更改当前代码设计的建议。
有一个已设置的管理器 class,它有一个由不同线程调用的计算方法。每个线程都有一个需要计算的资源。这些资源可能属于不同的公司。 对于每一个公司,我们都想缓存一些数据,但是在调用Calculate方法时,只能通过资源获取公司数据。
所以我目前的想法是在 Manager class 中有一个 Dictionary,以 companyResourceTag 作为 Key。 调用 Calculate 时,将确定 companyResourceTag 并调用 CheckCachedData 方法。
private void CheckCachedData(int companyResourceTag)
{
_ReaderWriterLock.EnterUpgradeableReadLock();
try
{
if (!_CompanyCachedData.ContainsKey(companyResourceTag))
{
Elements elements = ElementService.GetAllElements();
DataElements dataElements = ElementService.GetAllDataElements();
CalendarGroupings calendarGroupings = CalendarService.GetAllCalendarGroupings();
CachedDataContainer cachedItem = new CachedDataContainer(elements, dataElements, calendarGroupings);
_ReaderWriterLock.EnterWriteLock();
try
{
_CompanyCachedData.Add(companyResourceTag, cachedItem);
}
finally
{
_ReaderWriterLock.ExitWriteLock();
}
}
}
finally
{
_ReaderWriterLock.ExitUpgradeableReadLock();
}
}
如果以前没有来自该公司的资源,则必须通过服务获取该公司的数据。基础表不会经常更改,我们可以假设在所有资源的计算 运行 期间,表将保持不变。但是,获取这些数据非常耗时。因此需要缓存。
可能有 100 家不同的公司和 30000 多个资源需要计算。还有一些其他地方(每个资源)从中读取此缓存数据,例如:
_ReaderWriterLock.EnterReadLock();
try
{
_CompanyCachedData.TryGetValue(companyResourceTag, out cachedDataContainer);
}
finally
{
_ReaderWriterLock.ExitReadLock();
}
//Do something with cachedDataContainer
由于 Eric Lathrop 在这里的评论,我没有尝试使代码更优雅: Possible problem
我没有使用 ConcurrentDictionary,因为这里提到的问题:Possible problem with ConcurrentDictionary
我不确定普通锁是否会比 ReaderWriterLockSlim 更好,但我喜欢这样的想法,即一次可以有多个 reader,并且我可以升级锁。目前我也更关心正确性而不是速度。
我是否正确使用了 RWLS? 您是否同意我目前对 UpgradeableReadLock 的使用? 你同意我不使用 ConcurrentDictionary 的选择吗?
缺席 a good, minimal, complete code example 无法肯定地说明代码。但是,您发布的代码在基本正确性方面似乎没问题。
但是……
Would you agree on my current use of the UpgradeableReadLock?
我通常希望看到检索和缓存存储是同一操作的一部分。 IE。有一个主要负责检索值的方法,如果找不到,则以缓慢的方式获取值,将其存储在缓存中,然后 return.
将缓存检索和原始提取分成两个单独的方法似乎很奇怪并且可能有错误(但是,如果没有好的代码示例,我不能肯定地说)。
(注意:上面我只是指调用者看到的 API。当然,实际的缓存实现会将实际的数据获取分解为一个单独的方法。只是我希望看到由用于从缓存中检索值的相同方法调用的方法,这样您只需在值实际上已经在缓存中的情况下获取读锁一次).
Would you agree on my choice of not using ConcurrentDictionary?
同样,缺乏更多的上下文很难说。我倾向于尽量避免使用 ConcurrentDictionary
,因为与传统的 Dictionary<TKey, TValue>
class 相比,它具有更难使用的语义,至少在利用其特定于并发的功能时是这样。
但在您的情况下,我认为那些特定于并发的功能可能正是您正在寻找的。特别是,TValue GetOrAdd(TKey key, Func<TKey, TValue> valueFactory)
方法实现了缓存通常需要的语义:检索现有值,或者如果键不存在则用新值填充字典。
另一方面,该方法有一个重要警告:如果从多个线程同时调用该方法,则 valueFactory
委托可能会被调用多次。只有一个结果将实际用于填充字典,它可能会或可能不会影响本地性能,但它显然会影响您从中获取数据的服务器的整体负载。
如果这是一个问题,那么 ReaderWriterLockSlim
可能是更好的选择,因为它可以让您确保只有一个作者。
在第三方面,我会在此处记录我之前关于相对性能的评论。也就是说,尚不清楚 ReaderWriterLockSlim
的潜在性能优势在这里是否真的很重要。从理论上讲,与使用 Monitor
class (即 lock
语句)相比,已经填充的情况应该有更少的开销,但我认为你必须处理对于真正发挥作用的争论非常激烈。
从您的示例中甚至不清楚 companyResourceTag
如何映射到缓存键的给定数据组,不要介意您希望看到代码从缓存中检索已获取数据的频率(假设假定获取数据的成本,我认为缓存未命中的同步成本无关紧要)。因此,问题中缺少很多重要的细节,需要准确的答案。
我想如果存在 非常 高水平的竞争,并且未命中 非常 罕见,ReaderWriterLockSlim
可能 会给您带来可衡量的性能提升。但除此之外,我希望 lock
提供具有完全可接受的性能的更简单的代码。
当然,这纯属猜测。同样,如果没有完整的代码示例,就无法确定哪种同步机制最适合您的场景。但是考虑到获取数据的假设成本,管理自己的同步似乎确实比使用 ConcurrentDictionary<TKey, TValue>
更好,因为后者没有提供避免多次获取数据的直接方法。
您是否应该使用 ReaderWriterLockSlim
而不是更简单的 lock
方法,甚至更具推测性。 可能 前者更好,但由于缺乏更多信息,很可能不是。