Cry*_*ana 10 c# performance producer-consumer concurrent-queue system.threading.channels
ConcurrentQueue<T>我最近使用和构建了一个消费者/生产者系统SemaphoreSlim。然后利用新System.Threading.Channel类制作了另一个替代系统。
使用 BenchmarkDotNet 对两个系统进行基准测试后,将 1000 个项目写入两个系统 1000 次(并等待读者完成),我得到以下结果:
| Method | ItemsCount | Iterations | Mean | Error | StdDev | Median | Allocated |
|------------ |----------- |----------- |------------:|------------:|------------:|------------:|-----------:|
| MyQueue | 1000 | 1000 | 19,379.4 us | 1,230.30 us | 3,569.33 us | 18,735.6 us | 8235.02 KB |
| MyChannel | 1000 | 1000 | 45,858.2 us | 1,298.42 us | 3,704.46 us | 45,689.2 us | 72.11 KB |
Run Code Online (Sandbox Code Playgroud)
实施ConcurrentQueue似乎比Channel.
我尝试在频道上设置SingleReader和SingleWriterto true,但结果最终更糟:
| Method | ItemsCount | Iterations | Mean | Error | StdDev | Median | Allocated |
|------------ |----------- |----------- |------------:|------------:|------------:|------------:|-----------:|
| MyQueue | 1000 | 1000 | 18,578.7 us | 1,238.46 us | 3,493.10 us | 18,192.7 us | 8236.31 KB |
| MyChannel | 1000 | 1000 | 50,506.9 us | 1,383.73 us | 3,857.28 us | 49,635.8 us | 170.73 KB |
Run Code Online (Sandbox Code Playgroud)
我不确定我的实施或基准本身是否存在缺陷?如果不是,并且这些结果是有效的,那么什么时候应该优先选择 Channels 而不是普通的 ConcurrentQueue?
这两个类的简化代码如下所示:
| Method | ItemsCount | Iterations | Mean | Error | StdDev | Median | Allocated |
|------------ |----------- |----------- |------------:|------------:|------------:|------------:|-----------:|
| MyQueue | 1000 | 1000 | 19,379.4 us | 1,230.30 us | 3,569.33 us | 18,735.6 us | 8235.02 KB |
| MyChannel | 1000 | 1000 | 45,858.2 us | 1,298.42 us | 3,704.46 us | 45,689.2 us | 72.11 KB |
Run Code Online (Sandbox Code Playgroud)
对于渠道:
| Method | ItemsCount | Iterations | Mean | Error | StdDev | Median | Allocated |
|------------ |----------- |----------- |------------:|------------:|------------:|------------:|-----------:|
| MyQueue | 1000 | 1000 | 18,578.7 us | 1,238.46 us | 3,493.10 us | 18,192.7 us | 8236.31 KB |
| MyChannel | 1000 | 1000 | 50,506.9 us | 1,383.73 us | 3,857.28 us | 49,635.8 us | 170.73 KB |
Run Code Online (Sandbox Code Playgroud)
基准测试代码如下所示:
public class MyQueue
{
ConcurrentQueue<Item> _queue;
SemaphoreSlim _readerFinishedSemaphore;
SemaphoreSlim _readSemaphore;
bool completed = false;
public void Setup()
{
_queue = new();
_readerFinishedSemaphore = new(0);
_readSemaphore = new(0);
var task = new Task(Reader, TaskCreationOptions.LongRunning);
task.Start();
}
private async void Reader()
{
while (true)
{
await _readSemaphore.WaitAsync();
while (_queue.TryDequeue(out var item))
{
// do stuff ...
}
if (_completed) break;
}
_readerFinishedSemaphore.Release();
}
public void Write(IList<Item> items)
{
foreach (var i in items)
{
_queue.Enqueue(i);
}
_readSemaphore.Release();
}
public void CompleteAndWaitForReader()
{
_completed = true;
_readSemaphore.Release();
_readerFinishedSemaphore.Wait();
}
}
Run Code Online (Sandbox Code Playgroud)
应该注意的是我在 .NET 上运行它8.0.0-preview.6.23329.4
Cry*_*ana 17
执行速度更快的主要原因ConcurrentQueue<T>是,它每添加 1000 个项目才发出一次信号,而Channel<T>对每个项目都执行一次。
当我将基准调整为逐一添加 1000 个项目以使其更加公平时,结果几乎相同:
| Method | ItemsCount | Mean | Error | StdDev | Median | Allocated |
|---------- |----------- |---------:|---------:|---------:|---------:|----------:|
| MyQueue | 1000 | 163.8 us | 22.09 us | 64.44 us | 144.8 us | 8.42 KB |
| MyChannel | 1000 | 163.2 us | 14.02 us | 41.12 us | 177.9 us | 5.48 KB |
Run Code Online (Sandbox Code Playgroud)
在项目数量较多时,差异变得更加明显,有利于Channel<T>实施:(也特别是在分配方面)
| Method | ItemsCount | Mean | Error | StdDev | Median | Allocated |
|---------- |----------- |----------:|----------:|----------:|----------:|----------:|
| MyQueue | 10000 | 1.668 ms | 0.1971 ms | 0.5811 ms | 1.841 ms | 16.67 KB |
| MyChannel | 10000 | 1.163 ms | 0.1090 ms | 0.3197 ms | 1.121 ms | 9.92 KB |
| MyQueue | 100000 | 10.906 ms | 1.1151 ms | 3.1995 ms | 11.850 ms | 65.17 KB |
| MyChannel | 100000 | 6.678 ms | 0.2506 ms | 0.7026 ms | 6.653 ms | 9.92 KB |
Run Code Online (Sandbox Code Playgroud)
所以我想我会坚持Channel<T>一般生产者/消费者场景。