问题
I'm not sure if I'm stopping a Parallel.ForEach
loop as I intend to do.
So let me outline the problem.
The loop uses a database driver with limited available connections and it is required to keep track of the open connections, so the database driver doesn't throw an exception. The issue is that keeping track of open connections has been implemented manually (this should be refactored - writing a wrapper or using AutoResetEvent
but there are some other things that need to be taken care of first). So I need to keep track of the open connections and especially I have to handle the case of an exception:
Parallel.ForEach(hugeLists, parallelOptions, currentList => {
WaitForDatabaseConnection();
try {
Interlocked.Increment(ref numOfOpenConnections);
DoDatabaseCallAndInsertions();
} catch (Exception ex) {
// logging
throw;
} finally {
Interlocked.Decrement(ref numOfOpenConnections);
}
}
This is the simplified version of the loop without cancellation. To improve the performance in case of an Exception the loop should be cancelled as soon as possible when an Exception is thrown. If one thing fails the loop should stop.
How can I achieve that making sure that numOfOpenConnections
is being updated correctly?
What I have tried so far (is this behaving like I want it to or am I missing something?):
Parallel.ForEach(hugeLists, parallelOptions, (currentList, parallelLoopState) => {
parallelOptions.CancellationToken.ThrowIfCancellationRequested();
WaitForDatabaseConnection();
try {
Interlocked.Increment(ref numOfOpenConnections);
DoDatabaseCallAndInsertions();
} catch (Exception ex) {
// logging
cancellationTokenSource.Cancel();
parallelLoopState.Stop();
throw; // still want to preserve the original exception information
} finally {
Interlocked.Decrement(ref numOfOpenConnections);
}
}
I could wrap this code in a try - catch construct and catch AggregateException
.
回答1:
You could call the DoDatabaseCallAndInsertions
method in a way that waits for its completion only while the state of the loop is not exceptional, and otherwise forgets about it and allows the parallel loop to complete immediately. Using a cancelable wrapper is probably the simplest way to achieve this. Here is a method RunAsCancelable
that waits for a function to complete, or a CancellationToken
to become canceled, whatever comes first:
public static TResult RunAsCancelable<TResult>(Func<TResult> function,
CancellationToken token)
{
token.ThrowIfCancellationRequested();
Task<TResult> task = Task.Run(function, token);
try
{
// Wait for the function to complete, or the token to become canceled
task.Wait(token);
}
catch { } // Prevent an AggregateException to be thrown
token.ThrowIfCancellationRequested();
// Propagate the result, or the original exception unwrapped
return task.GetAwaiter().GetResult();
}
public static void RunAsCancelable(Action action, CancellationToken token)
=> RunAsCancelable<object>(() => { action(); return null; }, token);
The RunAsCancelable
method throws an OperationCanceledException
in case the token was canceled before the completion of the action
, or propagates the exception occurred in the action
, or completes successfully if the action
completed successfully.
Usage example:
using (var failureCTS = new CancellationTokenSource()) // Communicates failure
{
Parallel.ForEach(hugeLists, parallelOptions, (currentList, parallelLoopState) =>
{
WaitForDatabaseConnection();
try
{
Interlocked.Increment(ref numOfOpenConnections);
RunAsCancelable(() => DoDatabaseCallAndInsertions(failureCTS.Token),
failureCTS.Token);
}
catch (OperationCanceledException ex)
when (ex.CancellationToken == failureCTS.Token)
{
// Do nothing (an exception occurred in another thread)
}
catch (Exception ex)
{
Log.Error(ex);
failureCTS.Cancel(); // Signal failure to the other threads
throw; // Inform the parallel loop that an error has occurred
}
finally
{
Interlocked.Decrement(ref numOfOpenConnections);
}
});
}
The DoDatabaseCallAndInsertions
method can inspect the property IsCancellationRequested
of the CancellationToken
parameter at various points, and perform a transaction rollback if needed.
It should be noted that the RunAsCancelable
method is quite wasteful regarding the usage of ThreadPool
threads. One extra thread must be blocked in order to make each supplied action cancelable, so two threads are needed for each execution of the lambda. To prevent a possible starvation of the ThreadPool
, it is probably a good idea to increase the minimum number of threads that the thread pool creates on demand before switching to the create-one-every-500-msec algorithm, by using the ThreadPool.SetMinThreads method at the startup of the application.
ThreadPool.SetMinThreads(100, 10);
Important: The above solution makes no attempt to log the possible exceptions of the operations that have been forgotten. Only the exception of the first failed operation will be logged.
来源:https://stackoverflow.com/questions/62280038/stopping-parallel-foreach-with-cancellation-token-and-stop