如何加快矮胖的BlockingCollection实现

时间:2019-07-06 03:42:45

标签: c# .net multithreading performance blockingcollection

我已经多次使用BlockingCollection来实现生产者/消费者模式,但是由于相关的开销,我在使用极其精细的数据时遇到了糟糕的性能。这通常迫使我通过对数据进行分块/分区来即兴使用,换句话说,是使用BlockingCollection<T[]>而不是BlockingCollection<T>。这是resent example。这是可行的,但是很丑陋且容易出错。我最终在生产者和使用者上都使用了嵌套循环,并且我必须记住Add在生产者工作负载结束时还剩下什么。因此,我想到了实现一个块状BlockingCollection的想法,该块将在内部处理所有这些复杂性,并将与现有BlockingCollection的相同简单接口外部化。我的问题是我还没有设法匹配复杂的手动分区的性能。对于极细粒度的数据(基本上只是整数值),我仍然会尽最大努力支付大约100%的性能税。因此,我想在这里介绍我到目前为止所做的事情,希望能提供一些建议,以帮助我弥补性能差距。

我最好的尝试是使用ThreadLocal<List<T>>,以便每个线程都在专用的块上工作,从而消除了对锁的任何需求。

public class ChunkyBlockingCollection1<T>
{
    private readonly BlockingCollection<T[]> _blockingCollection;
    public readonly int _chunkSize;
    private readonly ThreadLocal<List<T>> _chunk;

    public ChunkyBlockingCollection1(int chunkSize)
    {
        _blockingCollection = new BlockingCollection<T[]>();
        _chunkSize = chunkSize;
        _chunk = new ThreadLocal<List<T>>(() => new List<T>(chunkSize), true);
    }

    public void Add(T item)
    {
        var chunk = _chunk.Value;
        chunk.Add(item);
        if (chunk.Count >= _chunkSize)
        {
            _blockingCollection.Add(chunk.ToArray());
            chunk.Clear();
        }
    }

    public void CompleteAdding()
    {
        var chunks = _chunk.Values.ToArray();
        foreach (var chunk in chunks)
        {
            _blockingCollection.Add(chunk.ToArray());
            chunk.Clear();
        }
        _blockingCollection.CompleteAdding();
    }

    public IEnumerable<T> GetConsumingEnumerable()
    {
        foreach (var chunk in _blockingCollection.GetConsumingEnumerable())
        {
            for (int i = 0; i < chunk.Length; i++)
            {
                yield return chunk[i];
            }
        }
    }
}

我的第二个最佳尝试是使用单个List<T>作为块,所有线程都使用锁以线程安全的方式访问该块。令人惊讶的是,这仅比ThreadLocal<List<T>>解决方案慢一点。

public class ChunkyBlockingCollection2<T>
{
    private readonly BlockingCollection<T[]> _blockingCollection;
    public readonly int _chunkSize;
    private readonly List<T> _chunk;
    private readonly object _locker = new object();

    public ChunkyBlockingCollection2(int chunkSize)
    {
        _blockingCollection = new BlockingCollection<T[]>();
        _chunkSize = chunkSize;
        _chunk = new List<T>(chunkSize);
    }

    public void Add(T item)
    {
        lock (_locker)
        {
            _chunk.Add(item);
            if (_chunk.Count >= _chunkSize)
            {
                _blockingCollection.Add(_chunk.ToArray());
                _chunk.Clear();
            }
        }
    }

    public void CompleteAdding()
    {
        lock (_locker)
        {
            _blockingCollection.Add(_chunk.ToArray());
            _chunk.Clear();
        }
        _blockingCollection.CompleteAdding();
    }

    public IEnumerable<T> GetConsumingEnumerable()
    {
        foreach (var chunk in _blockingCollection.GetConsumingEnumerable())
        {
            for (int i = 0; i < chunk.Length; i++)
            {
                yield return chunk[i];
            }
        }
    }
}

我还试图将ConcurrentBag<T>用作块,这导致性能下降和正确性问题(因为我没有使用锁)。另一尝试是将lock (_locker)替换为SpinLock,但性能更差。锁定显然是问题的根源,因为如果将其完全删除,则类将获得最佳性能。当然,对于多个生产者来说,解除锁定的失败很惨。


更新:我通过suggested实现了无锁解决方案Nick,并大量使用了Interlocked类。在具有一个生产者的配置中,性能稍好一些,但是对于两个或更多生产者,性能却差得多。不一致地发生许多冲突,导致线程旋转。该实现也非常棘手,易于引入错误。

public class ChunkyBlockingCollection3<T>
{
    private readonly BlockingCollection<(T[], int)> _blockingCollection;
    public readonly int _chunkSize;
    private T[] _array;
    private int _arrayCount;
    private int _arrayCountOfCompleted;
    private T[] _emptyArray;

    public ChunkyBlockingCollection3(int chunkSize)
    {
        _chunkSize = chunkSize;
        _blockingCollection = new BlockingCollection<(T[], int)>();
        _array = new T[chunkSize];
        _arrayCount = 0;
        _arrayCountOfCompleted = 0;
        _emptyArray = new T[chunkSize];
    }

    public void Add(T item)
    {
        while (true) // Spin
        {
            int count = _arrayCount;
            while (true) // Spin
            {
                int previous = count;
                count++;
                int result = Interlocked.CompareExchange(ref _arrayCount,
                    count, previous);
                if (result == previous) break;
                count = result;
            }
            var array = Interlocked.CompareExchange(ref _array, null, null);
            if (array == null) throw new InvalidOperationException(
                    "The collection has been marked as complete.");
            if (count <= _chunkSize)
            {
                // There is empty space in the array
                array[count - 1] = item;
                Interlocked.Increment(ref _arrayCountOfCompleted);
                break; // Adding is completed
            }
            if (count == _chunkSize + 1)
            {
                // Array is full. Push it to the BlockingCollection.
                while (Interlocked.CompareExchange(
                    ref _arrayCountOfCompleted, 0, 0) < _chunkSize) { } // Spin
                _blockingCollection.Add((array, _chunkSize));
                T[] newArray;
                while ((newArray = Interlocked.CompareExchange(
                    ref _emptyArray, null, null)) == null) { } // Spin
                Interlocked.Exchange(ref _array, newArray);
                Interlocked.Exchange(ref _emptyArray, null);
                Interlocked.Exchange(ref _arrayCountOfCompleted, 0);
                Interlocked.Exchange(ref _arrayCount, 0); // Unlock other threads
                Interlocked.Exchange(ref _emptyArray, new T[_chunkSize]);
            }
            else
            {
                // Wait other thread to replace the full array with a new one.
                while (Interlocked.CompareExchange(
                    ref _arrayCount, 0, 0) > _chunkSize) { } // Spin
            }
        }
    }

    public void CompleteAdding()
    {
        var array = Interlocked.Exchange(ref _array, null);
        if (array != null)
        {
            int count = Interlocked.Exchange(ref _arrayCount, -1);
            while (Interlocked.CompareExchange(
                ref _arrayCountOfCompleted, 0, 0) < count) { } // Spin
            _blockingCollection.Add((array, count));
            _blockingCollection.CompleteAdding();
        }
    }

    public IEnumerable<T> GetConsumingEnumerable()
    {
        foreach (var (array, count) in _blockingCollection.GetConsumingEnumerable())
        {
            for (int i = 0; i < count; i++)
            {
                yield return array[i];
            }
        }
    }
}

1 个答案:

答案 0 :(得分:1)

您可以尝试使用_chunk的数组,而不要使用List<T>。然后,您可以使用Interlocked.Increment递增要填充到Add上的下一个索引,并且当计数超过块的大小时,将其全部移至阻塞集合并在锁定中重置索引。