【问题标题】:Performance regarding cached thread-safe IEnumerable<T> implementation关于缓存线程安全 IEnumerable<T> 实现的性能
【发布时间】:2009-07-06 15:47:41
【问题描述】:

我创建了ThreadSafeCachedEnumerable&lt;T&gt; 类,旨在提高重复使用长时间运行的查询的性能。这个想法是从IEnumerable&lt;T&gt; 获取一个枚举器,并在每次调用MoveNext() 时将项目添加到缓存中。以下是我目前的实现:

/// <summary>
/// Wraps an IEnumerable&lt;T&gt; 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&lt;T&gt;(在 System.Interactive 程序集中找到),类似于我的 ThreadSafeCachedEnumerable&lt;T&gt;(排序)。

这是一个非常人为的示例,它非常缓慢地打印 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);
}

【问题讨论】:

标签: c# performance multithreading caching ienumerable


【解决方案1】:

一些建议:

  1. 现在普遍接受的做法是不让容器类负责锁定。例如,调用缓存枚举器的人可能还希望在枚举时阻止将新条目添加到容器中,这意味着锁定会发生两次。因此,最好将此责任推给调用者。
  2. 您的缓存依赖于始终按顺序返回项目的枚举器,这是无法保证的。最好使用DictionaryHashSet。同样,项目可能会在调用之间被删除,从而使缓存无效。
  3. 一般不建议在可公开访问的对象上建立锁。这包括包装的枚举器。例外是可以想象的,例如,当您绝对确定您是唯一持有对您正在枚举的容器类的引用的实例时。这也将在很大程度上使我在 #2 下的反对意见无效。

【讨论】:

  • +1 表示“现在普遍接受的做法是不让容器类负责锁定”。我希望每个人都能收到这份备忘录。
  • 这些都是不错的建议 - 以下是我的想法: 1) 这会为我的目的增加太多的复杂性。我只想缓存昂贵的 LINQ 投影的结果,我可能只需要其中的一些结果。一个延迟加载的列表,本质上,只是不能通过索引访问元素。不过,我同意你的常规容器。 2)出于我的目的,调用者应该将其理解为警告。 3)我无法想象一个 IEnumerable 持有对其一个 IEnumerators 的引用,但我认为这是一种可能性。感谢您的建议。
  • 普遍接受的做法,嗯?这是否解释了 .NET 4.0 中的新 System.Collections.Concurrent 命名空间?
  • 尚未详细研究 4.0,但从表面上看,新的命名空间似乎很好地表明“线程安全”容器是例外,而不是规则。但您可能是对的,“普遍接受的做法”是一个大胆的表达方式。
【解决方案2】:

.NET 中的锁定通常非常快(如果没有争用)。分析是否将锁定识别为性能问题的根源?在底层枚举器上调用MoveNext 需要多长时间?

此外,目前的代码不是线程安全的。您不能在一个线程(在if (currentIndex &lt; this.cachedItems.Count))上安全地调用this.cachedItems[currentIndex],同时在另一个线程上调用this.cachedItems.Add(current)。来自List(T) documentation:“只要不修改集合,一个 List(T) 可以同时支持多个读取器。”为了线程安全,您需要使用锁保护对this.cachedItems 的所有访问(如果有可能一个或多个线程可以修改它)。

【讨论】:

  • 这是一个有效的观点,但你知道是否会抛出异常吗?不锁定背后的想法是我可以指望列表只会越来越大并且索引会越来越大,所以我可以绕过锁定的需要 if (currentIndex
  • 我主要关心的是另一个线程调整列表内部数组的大小(在 Add() 中),而阅读器线程正在使用索引器检索项目。它似乎有可能返回 default(T) 或抛出 ArgumentOutOfRangeException。当然,这一切都取决于 List 的确切实现,所以我能说的最好的就是行为是“未定义的”。 (即使 Reflector 向您展示它是安全的,谁知道它是否会在 .NET 4.0 中发生变化,引入一个微妙且难以发现的错误?)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-11-29
  • 1970-01-01
  • 2012-06-22
  • 1970-01-01
相关资源
最近更新 更多