C# 尝试在长时间运行的发电机上使用PLINQ的陷阱?

C# 尝试在长时间运行的发电机上使用PLINQ的陷阱?,c#,system.reactive,plinq,C#,System.reactive,Plinq,我有一些无限生成器方法,包括一些长时间运行和无限长时间运行的生成器 IEnumerable<T> ExampleOne() { while(true) // this one blocks for a few seconds at a time yield return LongRunningFunction(); } IEnumerable<T> ExampleTwo() { while(true) //this one blocks

我有一些无限生成器方法,包括一些长时间运行和无限长时间运行的生成器

IEnumerable<T> ExampleOne() { 
    while(true) // this one blocks for a few seconds at a time
        yield return LongRunningFunction();
}
IEnumerable<T> ExampleTwo() { 
    while(true) //this one blocks for a really long time
        yield return OtherLongRunningFunction();
}
但是,有时(通常在运行了许多小时之后)
OtherLongRunningFunction()
会持续很长一段时间而不返回,并且在很难重现的条件下,组合的
序列将在其上阻塞,而不是继续从第一个
长运行函数返回结果。看起来,虽然组合并行查询开始时使用了两个线程,但后来它决定切换到一个线程


我的第一个想法是“这可能是RX
Observable.Merge
的工作,而不是PLINQ的工作。”但我希望两个答案都能说明处理这种情况的正确替代方法,并解释PLINQ如何在查询开始数小时后改变并行度的机制。

以下是Rx方法,它确实使用了
Merge

IObservable<T> LongRunningFunction()
{
    return Observable.Start(() => {
        // Calculate some stuff
        return blah;
    }, Scheduler.TaskPoolScheduler);
}

Observable.Merge(
    Observable.Defer(LongRunningFunction).Repeat(),
    Observable.Defer(OtherLongRunningFunction).Repeat(),
).Subscribe(x => {
    Console.WriteLine("An item: {0}", x);
});
IObservable LongRunningFunction()
{
返回可观察的。开始(()=>{
//计算一些东西
返回废话;
},Scheduler.TaskPoolScheduler);
}
可观察,合并(
Observable.Defer(LongRunningFunction).Repeat(),
Observable.Defer(OtherLongRunningFunction).Repeat(),
).订阅(x=>{
WriteLine(“一项:{0}”,x);
});

如果您想享受TPL的好处,特别是对于具有不同负载的任务(当您的订阅块和大量项目已生成时会发生什么情况-您是否应该停止生成项目?),我建议

如果要使用Rx执行此操作,对于真正长时间运行的计算任务,最好不要阻塞线程池:

var stream = Observable.Merge(ExampleTwo().ToObservable(Scheduler.NewThread), ExampleOne().ToObservable(Scheduler.NewThread));

stream.Subscribe(...);

关于PLINQ的力学:

我遇到了同样的问题:我有一个序列,它的项需要不均匀的处理时间,其中一些项需要更长的数量级。我经历了线程匮乏,在8核处理器上比在4核处理器上更容易复制,尽管在处理数小时后也可能发生在4核处理器上。一些线程可能会在一段时间后重新开始工作。请注意,使用了动态分块,如示例中所示

观察:饥饿更可能发生在连续运行很长时间的工作项完成时

MSDN主题透露了一些信息:

如果将并行循环与单独的步骤一起使用需要几秒钟或更长时间,请小心。这可能发生在I/O绑定的工作负载以及冗长的计算中。如果循环需要很长时间,您可能会遇到工作线程的无限增长,这是由于.NET ThreadPool类的线程注入逻辑使用了一种防止线程饥饿的启发式方法。当当前池的工作项长时间运行时,这种启发式方法会稳步增加工作线程的数量。其动机是在线程池中的所有内容都被阻塞的情况下添加更多线程。不幸的是,如果工作正在进行,那么更多的线程可能不一定是您想要的。NET框架无法区分这两种情况

我仍然不知道细节,但我认为底层ThreadPool的启发式方法不能很好地解释长时间运行的工作项,无法为下一次迭代容纳线程,因为某些上限没有正确调整,从而导致迭代排队。我没有visualstudio访问8核机器,在那里问题更容易再现。我还没有能够在4核机器上的VisualStudio调试下重现这个问题。调查仍在继续


关于更多细节,这个主题非常相关。

我不能说我对PLINQ有很多经验,所以这大部分都是无条件的猜测:看起来好像你假设将
ExampleOne()
ExampleTwo()
结合起来运行
aspallel()
在生成的
IEnumerable
上,将严格交替从第一个结果返回一个结果,从第二个结果返回一个结果。这一假设可能是错误的吗?如果是这样的话,您可能会遇到这样的情况:正在处理的序列看起来像是第1-2-1-2-2-1-2-2-2-1-2-2-2-1-2-2-2-1-2-2-2-1-2-2-2-1-2-2-2-2-1-2-2-2。。。这可以解释为什么你似乎被困在第二位。@Nailuj实际上更像我期望的1 1 2 1 1 2等,2是相当罕见的,连续2之间可能有很长的时间间隔。我预计组合序列将继续返回1s,事实上,大部分情况下实际上都是这样。但有时它也会停止返回1s。另一种想法是:PLINQ可能是从并行处理相等数量的1s和2s开始的,但由于1s完成得更快,它将显示为您描述的序列。然而,每当1结束时,该“点”将分别被1和2填满。结果是,一开始,你会觉得你得到了很多个1,而只有很少的2个,但从长远来看,正在处理的任务的“队列”将被2填满,因此似乎被卡住了?如果不是这样的话,有什么关系吗?只是想大声想一想:-)您是否确实验证了此行为(在执行两个长时间运行的函数期间添加一些诊断日志记录,包括当前线程ID)如果您在使用Rx时阻塞,那么您是做错了(tm)。垄断任务池并不像人们想象的那么糟糕,因为它是一个窃取任务的实现。也就是说,如果你知道你要在CPU密集型工作流上放置一堆线程,你可以做新线程的事情。@PaulBetts说得很好。您在TaskPool版本中编写了,所以我在线程版本中添加了。了解选择总是好的。
IObservable<T> LongRunningFunction()
{
    return Observable.Start(() => {
        // Calculate some stuff
        return blah;
    }, Scheduler.TaskPoolScheduler);
}

Observable.Merge(
    Observable.Defer(LongRunningFunction).Repeat(),
    Observable.Defer(OtherLongRunningFunction).Repeat(),
).Subscribe(x => {
    Console.WriteLine("An item: {0}", x);
});
var stream = Observable.Merge(ExampleTwo().ToObservable(Scheduler.NewThread), ExampleOne().ToObservable(Scheduler.NewThread));

stream.Subscribe(...);