Channel/BlockingCollection 分配免费替代方案?

gen*_*ray 2 c# multithreading asynchronous producer-consumer system.threading.channels

我最近对我的框架进行了基准测试,发现它分配了大量垃圾。

我正在使用 andChannel<T>TryRead操作ReadAsync在每次调用时分配内存。所以我用 a 交换了它,BlockingCollection<T>它也在 期间分配内存TryTake

我使用了一个带有单个写入器/读取器的无界通道。还有一个正常的BlockingCollection<T>

// Each thread runs this, jobmeta is justa struct 
while (!token.IsCancellationRequested)
{
    var jobMeta = await Reader.ReadAsync(token);  // <- allocs here
    jobMeta.Job.Execute();
    jobMeta.JobHandle.Notify();
}
Run Code Online (Sandbox Code Playgroud)

探查器告诉我,所有分配都是由该ChannelReader.ReadAsync方法引起的。不幸的是,我无法显示完整的代码,但是由于我在热路径中使用它们,所以我需要不惜一切代价避免分配。

是否有任何替代方案在读/写/获取期间不分配内存并且行为相同(生产者/消费者多线程的并发类)?我怎样才能自己实现一个?

The*_*ias 5

System.Threading.Channels库当前具有三个内置Channel<T>实现:

从这些实现来看,分配量越少BoundedChannel<T>。如果您不需要边界,可以使用 进行配置capacity: Int32.MaxValueUnboundedChannel<T>内部基于 a ,这ConcurrentQueue<T>是一个非常高性能且无争议的集合(它是无锁的)。分配是无锁的必要妥协。该BoundedChannel<T>基于内部Deque<T>集合,该集合与locks 同步。仅当必须扩展其后备数组的容量时,它才会分配内存,这种情况在通道的生命周期中只会发生几次。

BlockingCollection<T>默认情况下也是基于a ,ConcurrentQueue<T>因此具有相同的优点和缺点。如果您想减少分配(同时降低性能并增加争用),您可以IProducerConsumerCollection<T>基于synchronized实现 an Queue<T>,并将其作为参数传递给BlockingCollection<T>构造函数。您可以使用这个答案作为起点。

CancellationToken最后,无论如何,将 s传递给任何这些 API 都会导致分配。必须CancellationToken注册回调才能立即生效,并且不进行分配的回调是不可能的。我的建议是摆脱CancellationToken,并找到其他一些优雅地完成的方式。就像使用 theChannelWriter<T>.Complete或 theBlockingCollection<T>.CompleteAdding方法一样。