0

When using Parallel.ForEach(), is there a way to forcefully execute Thread.Abort on a specific thread?

I know that Thread.Abort() is not recommended.

I'm running a Parallel.ForEach() on a collection of a hundreds of thousands of entities.

The loop processes data going back 30 years in some cases. We've had a few issues where a thread hangs. While we are trying to get a grasp on that, was hoping to call implement a fail safe. If the thread runs for more than x amount of time, forcefully kill the thread.

I do not want to use a cancellation token.

It would be ugly, but haven't come to another solution. Would it be possible to:

  • Have each thread open a timer. Pass in reference of Thread.CurrentThread to timer
  • If the timer elapses, and processing hasn’t completed, call Thread.Abort on that timer
  • If needed, signal event wait handle to allow next patient to process
private void ProcessEntity(ProcessParams param,
    ConcurrentDictionary<long, string> entities)
{
    var options = new ParallelOptions
    {
        MaxDegreeOfParallelism = 2
    };
    Parallel.ForEach(person, options, p =>
    {
        ProcessPerson(param, p);
    });
}

internal void ProcessPerson(ProcessParams param, KeyValuePair<long, string> p)
{
    try
    {
        //...
    }
    catch (Exception ex)
    {

    }
    param.eventWaitHandle?.WaitOne();
}

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Hoppe
  • 6,508
  • 17
  • 60
  • 114
  • Why not to use [ParallelLoopState](https://learn.microsoft.com/ru-ru/dotnet/api/system.threading.tasks.parallelloopstate)? It is designed for your case – JL0PD Feb 05 '21 at 15:56
  • 3
    Can I ask why you don't want to use a cancellation token? It sounds like it's what cancellation tokens were made to do. https://stackoverflow.com/questions/22647242/timeout-for-action-in-parallel-foreach-iteration – Steve Norwood Feb 05 '21 at 15:57
  • 1
    Cancelling threads without cooperation from said thread is not recommended. You could possibly use a timeout and skip the result from the hanged thread, but this might leave a bunch of hanged threads around. I would very much recommend investigating *why* some operations are hanging? Is there some IO-operation, if so it might have a built in timeout you can use? Is it a deadlock? if so, fix it! – JonasH Feb 05 '21 at 16:00
  • @SteveNorwood if a thread gets into a deadlock state, or simply stops responding, I don't believe a cancellation token would work, correct? For a cancellation token, I believe I need to be in a working state where I can check the cancellation token and decide whether to proceed or cancel – Hoppe Feb 05 '21 at 16:22
  • @JL0PD ParallelLoopState doesn't seem to be much different than a cancellation token IMO. I still have to be in a working state where I have the ability to call Break() – Hoppe Feb 05 '21 at 16:31
  • @JonasH Yes I am investigating the root cause. This app cannot be deployed multiple times a day. I would like to have a fail safe in the meantime – Hoppe Feb 05 '21 at 16:33
  • Be aware that the [`Thread.Abort`](https://learn.microsoft.com/en-us/dotnet/api/system.threading.thread.abort) method is not supported on .NET 5 and .NET Core. The API is still here, but it throws a `PlatformNotSupportedException`. This limits your options. Aborting threads is out. Killing processes is in. – Theodor Zoulias Feb 05 '21 at 16:33
  • Great to know. I just added a tag. I'm on .net framework 4.6.1 for this app @TheodorZoulias – Hoppe Feb 05 '21 at 16:36
  • The only valid way to interrupt a task's thread, however it's created, is cooperatively. See duplicate. – Peter Duniho Feb 05 '21 at 21:53

1 Answers1

0

It seems that the Parallel.ForEach method is not resilient in the face of its worker threads being aborted, and behaves inconsistently. Other times it propagates an AggregateException that contains the ThreadAbortException, and other times it throws an ThreadAbortException directly, with an ugly stack trace revealing its internals.

The same happens with ThreadInterruptExceptions too. These exceptions are the result of the Thread.Interrupt method. Quoting from a recent GitHub issue:

Note that the vast majority of code, including in the core libraries, is not hardened against use of thread interrupts. These can cause exceptions to emerge from practically any place the thread is in a wait/sleep/join state, which means almost any blocking operation, time acquiring a lock, etc, and most code is not written to accommodate such asynchronous exceptions.

One way to solve this problem is to restrain the effect of Thread.Abort at specific code regions only, and not allow it to escape to code paths that you don't control. Starting from .NET 7, there is a new API ControlledExecution.Run that can be used for this purpose. This method takes an Action and a CancellationToken. In case the token is canceled while the action is running, the current thread is aborted. Then the ThreadAbortException is caught and handled, the abort is reset so that the thread can survive, and an OperationCanceledException is propagated that can be handled normally. Here is how it can be used:

Parallel.ForEach(persons, options, p =>
{
    using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)))
    {
        ControlledExecution.Run(() =>
        {
            ProcessPerson(param, p);
        }, cts.Token);
    }
});

This is a dangerous API, and Microsoft does not recommend using it in production code. It is intended mainly for test-related code.

Obviously the .NET Framework does not contain the ControlledExecution.Run API, but implementing it manually is not very difficult. You can find an implementation in this answer (the RunAbortable method).

The .NET Core and up to .NET 6 does not support aborting threads. On these platforms you can try interrupting threads instead of aborting them. It is not always effective and responsive, and it's not 100% safe either (as the aforementioned recent GitHub issue demonstrates). You can find a RunInterruptible implementation in the same answer. It can run on all .NET platforms, old and new.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104