[英]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.