我创建了 ThreadSafeCachedEnumerable<T>
旨在提高性能的类,其中长时间运行的查询被重用。我们的想法是从 IEnumerable<T>
中获取一个枚举器。并在每次调用 MoveNext()
时将项目添加到缓存中.以下是我目前的实现:
/// <summary>
/// Wraps an IEnumerable<T> and provides a thread-safe means of caching the values."/>
/// </summary>
/// <typeparam name="T"></typeparam>
class ThreadSafeCachedEnumerable<T> : IEnumerable<T>
{
// An enumerator from the original IEnumerable<T>
private IEnumerator<T> enumerator;
// The items we have already cached (from this.enumerator)
private IList<T> cachedItems = new List<T>();
public ThreadSafeCachedEnumerable(IEnumerable<T> enumerable)
{
this.enumerator = enumerable.GetEnumerator();
}
public IEnumerator<T> GetEnumerator()
{
// The index into the sequence
int currentIndex = 0;
// We will break with yield break
while (true)
{
// The currentIndex will never be decremented,
// so we can check without locking first
if (currentIndex < this.cachedItems.Count)
{
var current = this.cachedItems[currentIndex];
currentIndex += 1;
yield return current;
}
else
{
// If !(currentIndex < this.cachedItems.Count),
// we need to synchronize access to this.enumerator
lock (enumerator)
{
// See if we have more cached items ...
if (currentIndex < this.cachedItems.Count)
{
var current = this.cachedItems[currentIndex];
currentIndex += 1;
yield return current;
}
else
{
// ... otherwise, we'll need to get the next item from this.enumerator.MoveNext()
if (this.enumerator.MoveNext())
{
// capture the current item and cache it, then increment the currentIndex
var current = this.enumerator.Current;
this.cachedItems.Add(current);
currentIndex += 1;
yield return current;
}
else
{
// We reached the end of the enumerator - we're done
yield break;
}
}
}
}
}
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
}
我只是lock (this.enumerator)
当缓存中似乎没有更多项目时,以防万一另一个线程正要添加另一个项目(我假设从两个线程调用 this.enumerator 上的 MoveNext()
是个坏主意)。
检索以前缓存的项目时性能很好,但在第一次获取很多项目时(由于常量锁定)开始受到影响。有什么提高性能的建议吗?
编辑新的Reactive Framework使用 System.Linq.EnumerableEx.MemoizeAll()
解决了上述问题扩展方法。
在内部,MemoizeAll()
使用 System.Linq.EnumerableEx.MemoizeAllEnumerable<T>
(在 System.Interactive 程序集中找到),类似于我的 ThreadSafeCachedEnumerable<T>
(排序)。
这是一个非常人为的示例,它非常缓慢地打印 Enumerable 的内容(数字 1-10),然后第二次快速打印内容(因为它缓存了值):
// Create an Enumerable<int> containing numbers 1-10, using Thread.Sleep() to simulate work
var slowEnum = EnumerableEx.Generate(1, currentNum => (currentNum <= 10), currentNum => currentNum, previousNum => { Thread.Sleep(250); return previousNum + 1; });
// This decorates the slow enumerable with one that will cache each value.
var cachedEnum = slowEnum.MemoizeAll();
// Print the numbers
foreach (var num in cachedEnum.Repeat(2))
{
Console.WriteLine(num);
}
最佳答案
一些建议:
- 现在普遍接受的做法是不让容器类负责锁定。例如,调用您缓存的枚举器的人可能还想防止在枚举时将新条目添加到容器中,这意味着锁定会发生两次。因此,最好将该责任推给调用者。
- 您的缓存取决于枚举器始终按顺序返回项目,但不能保证这一点。最好使用
Dictionary
或HashSet
。同样,项目可能会在两次调用之间被删除,从而使缓存失效。 - 通常不建议在可公开访问的对象上建立锁。这包括包装的枚举器。异常(exception)是可以想象的,例如,当您绝对确定您绝对确定您是唯一持有对您正在枚举的容器类的引用的实例时。这也会在很大程度上覆盖我在 #2 下的反对意见。
关于c# - 关于缓存的线程安全 IEnumerable<T> 实现的性能,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/1087726/