It seems that the Parallel.ForEach
method is not resilient in the face of its worker threads being aborted, and behaves inconsistently. Other times propagates an AggregateException
that contains the ThreadAbortException
, and other times it throws an ThreadAbortException
directly, with an ugly stack trace revealing its internals.
Below is a custom ForEachTimeoutAbort
method that offers the basic functionality of the Parallel.ForEach
, without advanced features like cancellation, loop state, custom partitioners etc. Its special feature is the TimeSpan timeout
parameter, that aborts the worker thread of any item that takes too long to complete.
public static void ForEachTimeoutAbort<TSource>(
this IEnumerable<TSource> source,
Action<TSource> action,
int maxDegreeOfParallelism,
TimeSpan timeout)
{
// Arguments validation omitted
var semaphore = new SemaphoreSlim(maxDegreeOfParallelism, maxDegreeOfParallelism);
var exceptions = new ConcurrentQueue<Exception>();
try
{
foreach (var item in source)
{
semaphore.Wait();
if (!exceptions.IsEmpty) { semaphore.Release(); break; }
ThreadPool.QueueUserWorkItem(_ =>
{
var timer = new Timer(state => ((Thread)state).Abort(),
Thread.CurrentThread, Timeout.Infinite, Timeout.Infinite);
try
{
timer.Change(timeout, Timeout.InfiniteTimeSpan);
action(item);
}
catch (Exception ex) { exceptions.Enqueue(ex); }
finally
{
using (var waitHandle = new ManualResetEvent(false))
{
timer.Dispose(waitHandle);
// Wait the timer's callback (if it's queued) to complete.
waitHandle.WaitOne();
}
semaphore.Release();
}
});
}
}
catch (Exception ex) { exceptions.Enqueue(ex); }
// Wait for all pending operations to complete
for (int i = 0; i < maxDegreeOfParallelism; i++) semaphore.Wait();
if (!exceptions.IsEmpty) throw new AggregateException(exceptions);
}
A peculiarity of the ThreadAbortException
is that it cannot be caught. So in order to prevent the premature completion of the parallel loop, the Thread.ResetAbort
method must be called from the catch
block.
Usage example:
ForEachTimeoutAbort(persons, p =>
{
try
{
ProcessPerson(param, p);
}
catch (ThreadAbortException)
{
Thread.ResetAbort();
}
}, maxDegreeOfParallelism: 2, timeout: TimeSpan.FromSeconds(30));
The .NET Framework is the only platform where the ForEachTimeoutAbort
method could be used. For .NET Core and .NET 5 one could try converting it to ForEachTimeoutInterrupt
, and replace the call to Abort
with a call to Interrupt
. Interrupting a thread is not as effective as aborting it, because it only has effect when the thread is in a waiting/sleeping mode. But it may be sufficient in some scenarios.
与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…