簡體   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