在下面的代码中,我启动了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();
}
}
}
答案 0 :(得分:-1)
对BlockingCollection.Take()
的调用是一个阻塞调用,这意味着它会阻塞调用线程,请参阅the docs here。 produce
Task
的基础线程与Tasks
行ProcessQueue()
产生的许多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()
调用的所有线程都可以继续执行,克服线程资源饥饿。