什么时候应该优先选择 System.Threading.Channels 而不是 ConcurrentQueue?

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.

我尝试在频道上设置SingleReaderSingleWriterto 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>一般生产者/消费者场景。