Parallel.ForEach无法在长时间运行的IEnumerable上执行消息

时间:2011-10-19 16:49:18

标签: c# .net asynchronous task-parallel-library

为什么Parallel.ForEach在MoveNext返回false之前不会完成一系列任务?

我有一个工具可以监视传入消息的MSMQ和Service Broker队列的组合。找到消息后,它会将该消息传递给相应的执行程序。

我在IEnumerable中包装了对消息的检查,这样我就可以将Parallel.ForEach方法交给IEnumerable加上一个委托来运行。该应用程序旨在连续运行IEnumerator.MoveNext处理循环,直到它能够工作,然后IEnumerator.Current给它下一个项目。

由于MoveNext永远不会消失,直到我将CancelToken设置为true,这应该继续处理。相反,我看到的是,一旦Parallel.ForEach拾取了所有消息并且MoveNext不再返回“true”,则不再处理任务。相反,似乎MoveNext线程是在等待它返回时给予任何工作的唯一线程,而其他线程(包括等待和调度线程)不做任何工作。

  • 有没有办法告诉Parallel在等待MoveNext的响应时继续工作?
  • 如果没有,是否有另一种方法来构建MoveNext以获得我想要的东西? (让它返回true然后返回一个null对象的Current会产生很多伪造的任务)
  • Bonus问题:有没有办法限制Parallel一次拉出多少条消息?它似乎立即启动并安排了大量的消息(MaxDegreeOfParallelism似乎只限制了它一次完成的工作量,它并没有阻止它撤消大量的消息来安排)。

这是我编写的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是IEnumerable,它包含上面的IEnumerator,RunJob处理从该IEnumerator返回的TransportMessage:

var parallelOptions = new ParallelOptions
    {
        CancellationToken = _cancelTokenSource.Token,
        MaxDegreeOfParallelism = 8 
    };

Parallel.ForEach(_queueAccess, parallelOptions, x => RunJob(x));

3 个答案:

答案 0 :(得分:3)

听起来像Parallel.ForEach并不是你想要做的事情。我建议您使用BlockingCollection<T>来创建生产者/消费者队列 - 创建一堆线程/任务来为阻塞集合提供服务,并在它们到达时向其添加工作项。

答案 1 :(得分:1)

您的问题可能与正在使用的分区程序有关。

在您的情况下,TPL将选择Chunk Partitioner,它将从枚举中获取多个项目,然后再传递它们进行处理。每个块中的项目数量将随着时间的推移而增加。

当您的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();

答案 2 :(得分:0)

我没有努力确保这一点,但是我从Parallel.ForEach的讨论中得到的印象是它会把所有项目从可列举的数据中抽出来做出关于如何划分的适当决定他们跨线程。基于你的问题,这似乎是正确的。

因此,为了保留当前的大部分代码,您应该将阻塞代码从迭代器中拉出来,并将其置于对Parallel.ForEach(使用迭代器)的调用的循环中。