繁体   English   中英

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

[英]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线程是等待返回的唯一工作,而其他线程(包括等待和调度的线程)则不执行任何工作。

  • 有没有办法告诉Parallel在等待MoveNext的响应时继续工作?
  • 如果没有,是否有另一种方法来构造MoveNext以获得我想要的? (让它返回true,然后Current返回一个null对象会产生很多虚假的Tasks)
  • 奖励问题:有没有办法限制并行并行发送多少条消息? 似乎可以一次完成并调度大量消息(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是保存上述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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM