[英]Parallel.ForEach fails to execute messages on long running IEnumerable
为什么直到MoveNext返回false时Parallel.ForEach才能完成一系列任务?
我有一个工具可以监视MSMQ和Service Broker队列的组合,以接收传入消息。 找到一条消息后,它将把该消息交给适当的执行者。
我将对消息的检查包装在IEnumerable中,以便可以将Parallel.ForEach方法传递给IEnumerable以及要运行的委托。 该应用程序旨在通过IEnumerator.MoveNext处理在循环中连续运行,直到能够正常工作为止,然后是IEnumerator.Current给它下一个项目。
由于MoveNext直到我将CancelToken设置为true时才会消失,因此这应该永远继续进行。 相反,我所看到的是,一旦Parallel.ForEach拾取了所有消息并且MoveNext不再返回“ true”,就不再处理更多任务。 相反,似乎MoveNext线程是等待返回的唯一工作,而其他线程(包括等待和调度的线程)则不执行任何工作。
这是我所写内容的IEnumerator(不带任何多余的代码):
public class DataAccessEnumerator : IEnumerator<TransportMessage>
{
public TransportMessage Current
{ get { return _currentMessage; } }
public bool MoveNext()
{
while (_cancelToken.IsCancellationRequested == false)
{
TransportMessage current;
foreach (var task in _tasks)
{
if (task.QueueType.ToUpper() == "MSMQ")
current = _msmq.Get(task.Name);
else
current = _serviceBroker.Get(task.Name);
if (current != null)
{
_currentMessage = current;
return true;
}
}
WaitHandle.WaitAny(new [] {_cancelToken.WaitHandle}, 500);
}
return false;
}
public DataAccessEnumerator(IDataAccess<TransportMessage> serviceBroker, IDataAccess<TransportMessage> msmq, IList<JobTask> tasks, CancellationToken cancelToken)
{
_serviceBroker = serviceBroker;
_msmq = msmq;
_tasks = tasks;
_cancelToken = cancelToken;
}
private readonly IDataAccess<TransportMessage> _serviceBroker;
private readonly IDataAccess<TransportMessage> _msmq;
private readonly IList<JobTask> _tasks;
private readonly CancellationToken _cancelToken;
private TransportMessage _currentMessage;
}
这是Parallel.ForEach调用,其中_queueAccess是保存上述IEnumerator的IEnumerable,而RunJob处理从该IEnumerator返回的TransportMessage:
var parallelOptions = new ParallelOptions
{
CancellationToken = _cancelTokenSource.Token,
MaxDegreeOfParallelism = 8
};
Parallel.ForEach(_queueAccess, parallelOptions, x => RunJob(x));
在我看来,这听起来像Parallel.ForEach
并不是您想要做的事情的理想选择。 我建议您改为使用BlockingCollection<T>
来创建生产者/消费者队列-创建一堆线程/任务来服务于阻塞集合,并在到达时向其添加工作项。
您的问题可能与正在使用的分区程序有关。
在您的情况下,TPL将选择“块分区程序”,该程序将从枚举中提取多个项目,然后再将它们传递给处理。 每个块中占用的项目数将随时间增加。
当您的MoveNext
方法阻塞时,TPL会等待下一个项目,并且不会处理它已经采取的项目。
您可以通过以下几种方法解决此问题:
1)写一个总是返回单个项目的分区程序。 听起来不那么棘手。
2)使用TPL代替Parallel.ForEach
:
foreach ( var item in _queueAccess )
{
var capturedItem = item;
Task.Factory.StartNew( () => RunJob( capturedItem ) );
}
第二种解决方案稍微改变了行为。 foreach
循环将在创建所有Tasks
时完成,而不是在完成时完成。 如果您遇到问题,则可以添加CountdownEvent
:
var ce = new CountdownEvent( 1 );
foreach ( var item in _queueAccess )
{
ce.AddCount();
var capturedItem = item;
Task.Factory.StartNew( () => { RunJob( capturedItem ); ce.Signal(); } );
}
ce.Signal();
ce.Wait();
我并没有努力去确保这一点,但是我从Parallel.ForEach的讨论中得到的印象是,它将把所有项目从难以枚举的项目中抽出,并做出适当的决定,以决定如何在线程之间划分它们。 根据您的问题,这似乎是正确的。
因此,要保留大多数当前代码,您可能应该将阻塞代码从迭代器中拉出,并将其放入对Parallel.ForEach(使用迭代器)的调用的循环中。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.