考虑以下代码:

var channel = Channel.CreateUnbounded<int>(new UnboundedChannelOptions());

var t1 = Task.Run(async () =>
{
    DateTime start = DateTime.Now;

    for (int i = 0; i < 100000000; i++)
    {
        await channel.Writer.WriteAsync(i);
    }

    Console.WriteLine($"Writer took {DateTime.Now - start}");
    channel.Writer.Complete();
});

var t2 = Task.Run(async () =>
{
    while (true)
    {
        try
        {
            int r = await channel.Reader.ReadAsync();
        }
        catch (ChannelClosedException) { break; }
    }
});

await Task.WhenAll(t1, t2);

这大约需要10秒,即输出类似"Writer Take 00:00:10.276747"的内容.如果我把整个while块注释掉,大约需要6秒.这在多次运行中相当一致.

问题:如果Channel应该是一种有效的生产者/消费者机制,那么在这种情况下,为什么消费会影响生产者?

更奇怪的是,如果我加上这两种方法:

static async Task Produce(Channel<int> channel)
{
    DateTime start = DateTime.Now;

    for (int i = 0; i < 100000000; i++)
    {
        await channel.Writer.WriteAsync(i);
    }

    Console.WriteLine($"Writer took {DateTime.Now - start}");
    channel.Writer.Complete();
}

static async Task Consume(Channel<int> channel)
{
    while (true)
    {
        try
        {
            int r = await channel.Reader.ReadAsync();
        }
        catch (ChannelClosedException) { break; }
    }
}

然后做:

var t1 = Produce(channel);
var t2 = Consume(channel);
await Task.WhenAll(t1, t2);

无论哪种方式,它们都在大约6秒钟内完成(while块未注释与注释).

问题:为什么包含一个Task.Run的显式线程会影响效率?

推荐答案

这是一个有趣的问题,但不是因为缺乏效率.事实上,问题的数字表明通道非常有效.写入无界通道涉及:

  1. 写入内部ConcurrentQueue and
  2. 唤醒众多可能的读者之一进行通知.

这意味着排队and唤醒读取器只需要比简单地排队进入ConcurrentQueue多66%.这一点都不坏.不幸的是,这个数字具有欺骗性,尤其是在这种情况下,一个任务或ValueTask大于int个有效负载,并且"功"可以忽略不计.

基准库(如BenchmarkDotNet)多次运行测试,直到获得统计上稳定的样本,其中包括预热和冷却步骤,以考虑JIT、缓存和预热效果.

为了获得基准,我将BenchmarkDotnet与这个基准类一起使用.我忍不住为SingleReader优化添加了一个参数,该优化假设一次只能有一个读卡器,因此使用了更简单的队列和锁定.

[MemoryDiagnoser]
[ThreadingDiagnoser]
public class QuestionBenchmarks
{
    [Params(true, false)] // Arguments can be combined with Params
    public bool SingleReader;

    static async Task Produce(Channel<int> channel)
    {
        DateTime start = DateTime.Now;

        for (int i = 0; i < 100000000; i++)
        {
            await channel.Writer.WriteAsync(i);
        }
        channel.Writer.Complete();
    }

    static async Task Consume(Channel<int> channel)
    {
        while (true)
        {
            try
            {
                int r = await channel.Reader.ReadAsync();
            }
            catch (ChannelClosedException) { break; }
        }
    }

    [Benchmark]
    public async Task InlinedBoth()
    {
        var channel = Channel.CreateUnbounded<int>(new UnboundedChannelOptions { SingleReader = SingleReader });

        var t1 = Task.Run(async () =>
        {
            DateTime start = DateTime.Now;

            for (int i = 0; i < 100000000; i++)
            {
                await channel.Writer.WriteAsync(i);
            }

            channel.Writer.Complete();
        });

        var t2 = Task.Run(async () =>
        {
            while (true)
            {
                try
                {
                    int r = await channel.Reader.ReadAsync();
                }
                catch (ChannelClosedException) { break; }
            }
        });

        await Task.WhenAll(t1, t2);
    }

   
    [Benchmark]
    public async Task InlinedProduceOnly()
    {
        var channel = Channel.CreateUnbounded<int>(new UnboundedChannelOptions { SingleReader = SingleReader });

        var t1 = Task.Run(async () =>
        {
            DateTime start = DateTime.Now;

            for (int i = 0; i < 100000000; i++)
            {
                await channel.Writer.WriteAsync(i);
            }

            channel.Writer.Complete();
        });

        await t1;
    }


    [Benchmark]
    public async Task WithMethods()
    {
        var channel = Channel.CreateUnbounded<int>(new UnboundedChannelOptions { SingleReader = SingleReader });

        var producer = Produce(channel);
        var consumer = Consume(channel);

        await Task.WhenAll(producer, consumer);
    }

    [Benchmark]
    public async Task WithMethodsProduceOnly()
    {
        var channel = Channel.CreateUnbounded<int>(new UnboundedChannelOptions { SingleReader = SingleReader });

        var producer = Produce(channel);

        await producer;
    }

}

出乎意料的是:

// * Summary *

BenchmarkDotNet=v0.13.1, OS=Windows 10.0.22621
Intel Core i7-10850H CPU 2.70GHz, 1 CPU, 12 logical and 6 physical cores
.NET SDK=7.0.100-preview.5.22307.18
  [Host]     : .NET 6.0.6 (6.0.622.26707), X64 RyuJIT
  DefaultJob : .NET 6.0.6 (6.0.622.26707), X64 RyuJIT

有价值观

Method SingleReader Mean Error StdDev Completed Work Items Lock Contentions Gen 0 Gen 1 Gen 2 Allocated
InlinedBoth False 4.193 s 0.0825 s 0.0772 s 9675.0000 32071.0000 - - - 265 KB
InlinedProduceOnly False 3.842 s 0.0768 s 0.1654 s 1.0000 - 4000.0000 4000.0000 4000.0000 786,464 KB
WithMethods False 6.181 s 0.1233 s 0.2027 s - - 4000.0000 4000.0000 4000.0000 786,463 KB
WithMethodsProduceOnly False 3.805 s 0.0753 s 0.0837 s - - 4000.0000 4000.0000 4000.0000 786,462 KB
InlinedBoth True 4.342 s 0.1200 s 0.3483 s 84484.0000 22848.0000 - - - 71 KB
InlinedProduceOnly True 2.990 s 0.0595 s 0.0908 s 1.0000 - 3000.0000 3000.0000 3000.0000 393,230 KB
WithMethods True 4.158 s 0.0814 s 0.1426 s - - 3000.0000 3000.0000 3000.0000 393,230 KB
WithMethodsProduceOnly True 2.879 s 0.0547 s 0.0512 s - - 3000.0000 3000.0000 3000.0000 393,232 KB

Completed Work Items是线程池中完成的任务数.带有方法的基准测试根本不使用线程池.当然他们不会,因为他们不使用任务.跑使用方法的代码不使用多个线程,因此没有锁冲突.与没有生产者的代码相同.

这意味着无法比较基准.即使如此,很明显,使用SingleReader使用更少的内存

包含100万个项目的整个基准测试耗时28分钟,所以在创建一个包含更少项目的新的、正确的基准测试之前,我将稍等片刻

Global total time: 00:28:23 (1703.72 sec), executed benchmarks: 10

Csharp相关问答推荐

PredicateBuilder不是循环工作,而是手动工作

应该使用哪一个?"_counter += 1 OR互锁增量(ref_counter)"""

Blazor Foreach仅渲染最后一种 colored颜色

Nuget包Serilog.Sinks.AwsCloudwatch引发TypeLoadExceptions,因为父类型是密封的

.NET 6控制台应用程序,RabbitMQ消费不工作时,它的程序文件中的S

如何捕获对ASP.NET核心应用程序的所有请求并将其发送到一个页面

在IAsyncEnumerable上先调用,然后跳过(1)可以吗?

在.NET 7.0 API控制器项目中通过继承和显式调用基类使用依赖项注入

当试图限制EF Select 的列时,如何避免重复代码?

无法将生产环境的AppDbContext设置替换为用于集成测试的内存数据库

等待一个等待函数

在C#中,是否有与变量DISARD对应的C++类似功能?

使用CollectionView时在.NET Maui中显示数据时出现问题

在使用UserManager时,如何包含与其他实体的关系?

如何在microsoft.clearscript.v8的jsondata中使用Linq

Azure Functions v4中的Serilog控制台主题

未在Windows上运行的Maui项目

我应该为C#12中的主构造函数参数创建私有属性吗?

为什么在使用JsonDerivedType序列化泛型时缺少$type?

try 创建一个C#程序,该程序使用自动实现的属性、覆盖ToString()并使用子类