Search code examples
c#parallel-processing.net-7.0cancellationparallel.invoke

How forcefully end Parallel.Invoke loop?


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.


Solution

  • 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.