我已经多次使用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];
}
}
}
}
答案 0 :(得分:1)
您可以尝试使用_chunk
的数组,而不要使用List<T>
。然后,您可以使用Interlocked.Increment递增要填充到Add
上的下一个索引,并且当计数超过块的大小时,将其全部移至阻塞集合并在锁定中重置索引。