C# 为什么首次向BlockingCollection添加项目时会延迟近一分钟

C# 为什么首次向BlockingCollection添加项目时会延迟近一分钟,c#,task-parallel-library,C#,Task Parallel Library,在下面的代码中,我启动了50个消费者任务来处理我添加到BlockingCollection中的操作(在我的业务流程中,生产速度很快,消费速度要慢得多)。问题是程序启动时有很长的延迟,之后它会正常运行。我注意到,当我减少消费者任务的数量时,延迟会缩短。下面的代码是一个完整的工作示例。只需将此代码粘贴到控制台应用程序中。我已经用.NETCore2和.NETFramework4.7对此进行了测试 class Program { private QueueProcessor queueProce

在下面的代码中,我启动了50个消费者任务来处理我添加到BlockingCollection中的操作(在我的业务流程中,生产速度很快,消费速度要慢得多)。问题是程序启动时有很长的延迟,之后它会正常运行。我注意到,当我减少消费者任务的数量时,延迟会缩短。下面的代码是一个完整的工作示例。只需将此代码粘贴到控制台应用程序中。我已经用.NETCore2和.NETFramework4.7对此进行了测试

class Program
{
    private QueueProcessor queueProcessor = new QueueProcessor();

    static void Main(string[] args)
    {
        new Program().Run();
    }

    public void Run()
    {
        Console.WriteLine("Press esc to cancel");
        int consumerCount = 50;  //change this to 5 or...
        Task produce = Produce();
        Task consume = queueProcessor.ProcessQueue(consumerCount);

        try
        {

            Console.WriteLine("Waiting on producer and consumers.");
            Task.WaitAll(produce, consume);
        }
        catch (Exception ex)
        {
            string a = ex.Message;
        }
        queueProcessor.Dispose();
        Console.WriteLine("Done Producing and Consuming.  Press any key...");
        Console.ReadKey();
    }

    private async Task Produce()
    {
        while (true)
        {
            if (Console.KeyAvailable && Console.ReadKey(true).Key == ConsoleKey.Escape)
                break;

            Console.WriteLine("Producing");
            await Task.Delay(1);
            string g = Guid.NewGuid().ToString();
            queueProcessor.Add(() => Consume(g));
        }
        queueProcessor.CompleteAdding();
        Console.WriteLine("Done producing");
    }

    private async Task Consume(string s)
    {
        await Task.Delay(500);
        Console.WriteLine("Consuming: " + s);
    }

}

public class QueueProcessor : BlockingCollection<Func<Task>> 
{

    public Task ProcessQueue(int consumerCount = 1)
    {
        if (consumerCount < 1)
            throw new Exception("consumerCount must be greater than zero.");

        Task[] tasks = new Task[consumerCount];

        for (int i = 0; i < consumerCount; i++)
            tasks[i] = Task.Run(Consume);

        return Task.WhenAll(tasks);
    }


    private async Task Consume()
    {
        while (true)
        {
            Func<Task> a = null;

            try
            {
                a = Take();
            }
            catch (InvalidOperationException)
            {
                break;
            }

            await a();
        }
    }
}
类程序
{
private QueueProcessor QueueProcessor=新的QueueProcessor();
静态void Main(字符串[]参数)
{
新程序().Run();
}
公开募捐
{
控制台写入线(“按esc键取消”);
int consumerCount=50;//将其更改为5或。。。
任务生产=生产();
任务消耗=queueProcessor.ProcessQueue(consumerCount);
尝试
{
Console.WriteLine(“等待生产者和消费者”);
Task.WaitAll(生产、消费);
}
捕获(例外情况除外)
{
字符串a=例如消息;
}
queueProcessor.Dispose();
Console.WriteLine(“完成生产和消费。按任意键…”);
Console.ReadKey();
}
专用异步任务生成()
{
while(true)
{
if(Console.KeyAvailable&&Console.ReadKey(true).Key==ConsoleKey.Escape)
打破
控制台。写入线(“生成”);
等待任务。延迟(1);
字符串g=Guid.NewGuid().ToString();
queueProcessor.Add(()=>Consume(g));
}
queueProcessor.CompleteAdding();
控制台。写入线(“完成制作”);
}
专用异步任务消耗(字符串s)
{
等待任务。延迟(500);
控制台写入线(“消费:+s”);
}
}
公共类队列处理器:BlockingCollection
{
公共任务处理队列(int consumerCount=1)
{
如果(消费者计数<1)
抛出新异常(“consumerCount必须大于零”);
任务[]任务=新任务[consumerCount];
对于(int i=0;i
BlockingCollection.Take()的调用是一个阻塞调用,这意味着它会阻塞调用线程,请参阅。
product
Task
的底层线程与
ProcessQueue()
Tasks[i]=Task.Run(Consume)行生成的许多
任务之间存在竞争条件及其处理的线程。在应用程序运行的早期,这可能会导致所有当前线程阻塞,等待
Take()
返回值,或者主线程等待
Task.WaitAll(生产、消费)
完成,有效地锁定应用程序

但是等等!为什么应用程序最终会打破这个死锁并继续执行?要回答这个问题,我们需要看一下。当应用程序启动时,CLR会捕获许多系统线程(在我的笔记本电脑上,早期存在1个主线程+4个工作线程)。如果发生死锁,则在调用
Take()
(加上
Task.WaitAll(product,consume);
时有4个工作线程阻塞,没有线程为
product
任务提供服务


在这种情况下,糟糕的CLR该怎么办?当CLR的工作线程池检测到当前线程被阻止或仍有任务等待运行时,线程池将注入新线程以处理这些任务。线程池注入速率有限(对于我的环境,大约每秒一个新线程)。不幸的是,对于这段代码,CLR似乎总是将一个等待的
Consume
任务调度到新线程,导致它也立即阻塞。不过,线程池最终将注入足够的线程,使所有50个
消费
任务等待,此时下一个新线程可以运行休眠
生产者
任务。好极了由于
生产者
能够再次运行,等待
Take()
调用的所有线程都可以恢复执行,从而克服线程资源不足的问题。

“在应用程序运行的早期,这可能会导致所有当前线程阻塞..有效地死锁应用程序。”这是我不理解的。制作人和一些消费者一开始就有了线索,为什么他们不能在创造新消费者的同时继续前进呢。我想一个更直接的问题是“在产品线程启动后是什么阻止了它?”这是一个很好的答案,谢谢。
product
线程至少运行一次,但是,当它在调用
wait Task时产生它的线程。Delay(1)
,另一个
消费者可以运行并完全阻止该线程,如果您将
等待任务。延迟
线程。睡眠(1)
并通过
任务运行
生产
任务。运行
,则
生产
方法将阻塞,而不是生成其线程,允许项目向前推进我现在正在做的事情!我相信你是对的,这就是问题所在!当然,等待导致产品的线程被许多消费者劫持,再也没有返回。谢谢你,约翰·内森。