Inside the Compute
function there are heavy, long-lasting calculations (dozens of minutes). I can't edit this function, I don't have access to the source code.
List<Action> parallelActions = new();
for (int i = 0; i < 1000; i++)
{
parallelActions.Add(() => Compute(i));
}
Parallel.Invoke(parallelActions.ToArray());
I would like to stop the loop execution if it takes longer than maxTime
seconds. Do you know how to end a parallel loop by immediately stopping execution?
I tried adding CancellationToken
:
List<Action> parallelActions = new();
for (int i = 0; i < 1000; i++)
{
parallelActions.Add(() => Compute(i));
}
using CancellationTokenSource cts = new(new TimeSpan(0, 0, maxTime));
ParallelOptions parallelOptions = new() { CancellationToken = cts.Token };
Parallel.Invoke(parallelOptions, parallelActions.ToArray());
The problem is that even if I add CancellationToken
, already started Compute
functions will not stop, so I have to wait a very long time for it to finish. How to change it?
More info: I am targeting the .NET 7 platform. Each individual Compute
invocation takes dozens of minutes to complete on average.
The recommended way for aborting forcefully work in a non-cooperative fashion, is to run the work on a separate executable and abort the work by killing the process. This is the only way to protect the state of you main executable from corruption. Even if you are targeting a .NET platform that supports the Thread.Abort
API, like the .NET Framework, by using it you risk the stability of your application and the correctness of its behavior.
.NET 7 has introduced the ControlledExecution.Run
method, that performs a controlled Thread.Abort
without actually terminating the current thread. The termination is canceled automatically with the Thread.ResetAbort
. Using the ControlledExecution.Run
method generates a compilation warning:
The
ControlledExecution.Run(Action, CancellationToken)
method might corrupt the process, and should not be used in production code.
Although the Thread.Abort
is "controlled", it can corrupt the state of your application just as easily as the raw Thread.Abort
. The thread is aborted at any arbitrary moment while executing individual CPU instructions. Not methods, CPU instructions. Most .NET APIs are compiled to multiple CPU instructions, and are not hardened for recovering after a partial execution of their instruction set.
Assuming that you understand the risks, here is how you can use it:
using CancellationTokenSource cts = new();
ParallelOptions options = new()
{
CancellationToken = cts.Token,
MaxDegreeOfParallelism = 20 // Configurable
};
ThreadPool.GetMinThreads(out _, out int cpt);
ThreadPool.SetMinThreads(options.MaxDegreeOfParallelism, cpt);
cts.CancelAfter(TimeSpan.FromMinutes(120));
Parallel.For(0, 1000, options, index =>
{
ControlledExecution.Run(() =>
{
Compute(index);
}, cts.Token);
});
In the above example the ThreadPool.SetMinThreads
configures the ThreadPool
, so that it creates immediately all the threads that the Parallel.For
needs in order to reach its desirable degree of parallelism from the get-go. If you omit it, the ThreadPool
might take some time before creating the required number of threads.
An alternative idea is to bypass the ThreadPool
and instead use dedicated threads. You can find here a custom TaskScheduler
that creates a dedicated background thread per task.