为什么在首次向BlockingCollection添加项目时会有几乎一分钟的延迟

时间:2017-11-02 23:40:37

标签: c# task-parallel-library

在下面的代码中,我启动了50个消费者任务来处理我添加到BlockingCollection的动作(在我的业务流程中,生成快速消耗的速度要慢得多)。问题是程序启动时会有很长的延迟,之后会正常运行。我注意到,当我减少消费者任务的数量时,延迟会缩短。下面的代码是一个完整的工作示例。只需将此代码粘贴到控制台应用中即可。我用.net core 2和.net framework 4.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();
        }
    }
}

1 个答案:

答案 0 :(得分:-1)

BlockingCollection.Take()的调用是一个阻塞调用,这意味着它会阻塞调用线程,请参阅the docs hereproduce Task的基础线程与TasksProcessQueue()产生的许多tasks[i] = Task.Run( Consume );之间存在竞争条件它们被处理的线程。在应用程序运行的早期,这可能导致所有当前线程阻塞等待Take()返回值或等待Task.WaitAll( produce, consume );完成的主线程,从而有效地使应用程序死锁。

但是等等!为什么应用程序最终会摆脱这种死锁并继续执行?要回答这个问题,我们需要查看Common Language Runtime's (CLR) thread pool and thread-injection algorithm。当应用程序启动时,CLR会抓取许多系统线程(在我的笔记本电脑上,早期存在1个主要+ 4个工作线程)。如果发生死锁,则在调用Take()时加上4个工作线程阻塞(加上Task.WaitAll( produce, consume );上阻塞的主线程),没有线程为produce任务提供服务。

在这种情况下,做什么不好的CLR? Add more threads of course!当CLR的工作线程池检测到当前线程被阻塞或仍有Tasks等待运行时,线程池将注入新线程来处理这些任务。线程池注入是速率限制的(我的环境每秒大约一个新线程)。不幸的是,对于这段代码,CLR似乎始终如一地将等待Consume任务之一安排到新线程,导致它立即阻塞。但最终,线程池将注入足够的线程以使所有50个Consume任务等待,此时下一个新线程可以运行休眠producer任务。万岁!在producer能够再次运行的情况下,等待Take()调用的所有线程都可以继续执行,克服线程资源饥饿。