枚举器包装器,用于预先缓冲来自底层枚举器的多个项目

时间:2015-06-08 04:11:05

标签: c# .net enumerator backpressure

假设我有一些IEnumerator<T>MoveNext()方法中进行了大量处理。

从该枚举器消耗的代码不仅消耗数据可用的速度,而且偶尔等待(具体与我的问题无关),以便同步需要恢复消耗的时间。但是当它下次调用MoveNext()时,它需要尽可能快的数据。

一种方法是将整个流预先消耗到一些列表或数组结构中以进行即时枚举。然而,这将是对内存的浪费,因为在任何单个时间点,只有一个项目在使用,并且在整个数据不适合内存的情况下,这将是禁止的。

所以在.net中有一些通用的东西,包含一个枚举器/可枚举的方式,它提前异步预先迭代底层枚举器几个项目并缓冲结果以便它总是缓冲区中有多个项目,调用MoveNext永远不会等待?显然,消耗的项目,即来自调用者的后续MoveNext迭代的项目,将从缓冲区中删除。

NB 我正在尝试做的部分也被称为 Backpressure ,而且在Rx世界中,已经在RxJava中实现了正在讨论Rx.NET。 Rx(推送数据的可观察量)可以被认为是枚举器的相反方法(枚举器允许提取数据)。在拉动方法中背压相对容易,我的答案显示:只是暂停消费。推动时更难,需要额外的反馈机制。

2 个答案:

答案 0 :(得分:2)

自定义可枚举类的更简洁的替代方法是:

public static IEnumerable<T> Buffer<T>(this IEnumerable<T> source, int bufferSize)
{
    var queue = new BlockingCollection<T>(bufferSize);

    Task.Run(() => {
        foreach(var i in source) queue.Add(i);
        queue.CompleteAdding();
    });

    return queue.GetConsumingEnumerable();
}

这可以用作:

var slowEnumerable = GetMySlowEnumerable();
var buffered = slowEnumerable.Buffer(10); // Populates up to 10 items on a background thread

答案 1 :(得分:0)

有不同的方法可以自己实现,我决定使用

  • 执行异步预缓冲的每个枚举器的单个专用线程
  • 预先缓冲的固定数量的元素

这对我手边的情况来说是完美的(只有少数,长期运行的调查员),但是如果你使用大量的枚举器,创建一个线程可能太重了,如果你需要一些更动态的东西,可能根据项目的实际内容,固定数量的元素可能太不灵活。

到目前为止,我只测试了它的主要功能,并且可能会留下一些粗糙的边缘。它可以像这样使用:

int bufferSize = 5;
IEnumerable<int> en = ...;
foreach (var item in new PreBufferingEnumerable<int>(en, bufferSize))
{
    ...

这是枚举器的要点:

class PreBufferingEnumerator<TItem> : IEnumerator<TItem>
{
    private readonly IEnumerator<TItem> _underlying;
    private readonly int _bufferSize;
    private readonly Queue<TItem> _buffer;
    private bool _done;
    private bool _disposed;

    public PreBufferingEnumerator(IEnumerator<TItem> underlying, int bufferSize)
    {
        _underlying = underlying;
        _bufferSize = bufferSize;
        _buffer = new Queue<TItem>();
        Thread preBufferingThread = new Thread(PreBufferer) { Name = "PreBufferingEnumerator.PreBufferer", IsBackground = true };
        preBufferingThread.Start();
    }

    private void PreBufferer()
    {
        while (true)
        {
            lock (_buffer)
            {
                while (_buffer.Count == _bufferSize && !_disposed)
                    Monitor.Wait(_buffer);
                if (_disposed)
                    return;
            }
            if (!_underlying.MoveNext())
            {
                lock (_buffer)
                    _done = true;
                return;
            }
            var current = _underlying.Current; // do outside lock, in case underlying enumerator does something inside get_Current()
            lock (_buffer)
            {
                _buffer.Enqueue(current);
                Monitor.Pulse(_buffer);
            }
        }
    }

    public bool MoveNext()
    {
        lock (_buffer)
        {
            while (_buffer.Count == 0 && !_done && !_disposed)
                Monitor.Wait(_buffer);
            if (_buffer.Count > 0)
            {
                Current = _buffer.Dequeue();
                Monitor.Pulse(_buffer); // so PreBufferer thread can fetch more
                return true;
            }
            return false; // _done || _disposed
        }
    }

    public TItem Current { get; private set; }

    public void Dispose()
    {
        lock (_buffer)
        {
            if (_disposed)
                return;
            _disposed = true;
            _buffer.Clear();
            Current = default(TItem);
            Monitor.PulseAll(_buffer);
        }
    }