From my ASP.NET Core 2.2 application, I am having to call a 3rd party library that is prone to pushing the CPU to 100% and basically hanging the machine - happens at least twice a month. I have no access to the source nor will the vendor fix it.
My solution to this problem was to isolate this 3rd party library in a .NET Framework 4.x web service where I can call Thread.Abort it if I detect issues. The reason for isolating it in a .NET Framework service rather than .NET Core, is because the latter doesn't support Thread.Abort. The current solution, while not ideal, works. Even knowing that Thread.Abort could cause instability (hasn't so far).
I'd rather not have the library isolated for performance reasons. But so far, I haven't found a way to kill a runaway thread (or Task) in a .NET Core project.
What are the alternatives that are available to me?
Abort to abort a thread other than the current thread, you don't know what code has executed or failed to execute when the ThreadAbortException is thrown. You also cannot be certain of the state of your application or any application and user state that it's responsible for preserving. For example, calling Thread.
Abort method throws a ThreadAbortException, the Thread. Interrupt method throws a ThreadInterruptException. Essentially, a call to the Thread. Interrupt method interrupts the thread and throws a ThreadInterruptedException to interrupt the thread inside of a blocking call.
NET class libraries are not thread safe by default. Avoid providing static methods that alter static state. In common server scenarios, static state is shared across requests, which means multiple threads can execute that code at the same time. This opens up the possibility of threading bugs.
I also agree with the comment that tearing down the whole process might be a more clean solution in this case. However, if you prefer to stick with the Thread.Abort
approach, it's not difficult to implement it with .NET Core for Windows at least, using Win32 Interop to call unmanaged TerminateThread
API.
Below is an example of doing that (warning: almost untested).
using System;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
namespace CoreConsole
{
class Program
{
static async Task Main(string[] args)
{
try
{
using (var longRunningThread = new LongRunningThread(() => Thread.Sleep(5000)))
{
await Task.Delay(2500);
longRunningThread.Abort();
await longRunningThread.Completion;
Console.WriteLine("Finished");
}
}
catch (Exception ex)
{
Console.WriteLine($"{ex.Message}");
}
}
}
public class LongRunningThread : IDisposable
{
readonly Thread _thread;
IntPtr _threadHandle = IntPtr.Zero;
readonly TaskCompletionSource<bool> _threadEndTcs;
readonly Task _completionTask;
public Task Completion { get { return _completionTask; } }
readonly object _lock = new object();
public LongRunningThread(Action action)
{
_threadEndTcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
_thread = new Thread(_ =>
{
try
{
var hCurThread = NativeMethods.GetCurrentThread();
var hCurProcess = NativeMethods.GetCurrentProcess();
if (!NativeMethods.DuplicateHandle(
hCurProcess, hCurThread, hCurProcess, out _threadHandle,
0, false, NativeMethods.DUPLICATE_SAME_ACCESS))
{
throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
}
action();
_threadEndTcs.TrySetResult(true);
}
catch (Exception ex)
{
_threadEndTcs.TrySetException(ex);
}
});
async Task waitForThreadEndAsync()
{
try
{
await _threadEndTcs.Task.ConfigureAwait(false);
}
finally
{
// we use TaskCreationOptions.RunContinuationsAsynchronously for _threadEndTcs
// to mitigate possible deadlocks here
_thread.Join();
}
}
_thread.IsBackground = true;
_thread.Start();
_completionTask = waitForThreadEndAsync();
}
public void Abort()
{
if (Thread.CurrentThread == _thread)
throw new InvalidOperationException();
lock (_lock)
{
if (!_threadEndTcs.Task.IsCompleted)
{
_threadEndTcs.TrySetException(new ThreadTerminatedException());
if (NativeMethods.TerminateThread(_threadHandle, uint.MaxValue))
{
NativeMethods.WaitForSingleObject(_threadHandle, NativeMethods.INFINITE);
}
else
{
throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
}
}
}
}
public void Dispose()
{
if (Thread.CurrentThread == _thread)
throw new InvalidOperationException();
lock (_lock)
{
try
{
if (_thread.IsAlive)
{
Abort();
_thread.Join();
}
}
finally
{
GC.SuppressFinalize(this);
Cleanup();
}
}
}
~LongRunningThread()
{
Cleanup();
}
void Cleanup()
{
if (_threadHandle != IntPtr.Zero)
{
NativeMethods.CloseHandle(_threadHandle);
_threadHandle = IntPtr.Zero;
}
}
}
public class ThreadTerminatedException : SystemException
{
public ThreadTerminatedException() : base(nameof(ThreadTerminatedException)) { }
}
internal static class NativeMethods
{
public const uint DUPLICATE_SAME_ACCESS = 2;
public const uint INFINITE = uint.MaxValue;
[DllImport("kernel32.dll")]
public static extern IntPtr GetCurrentThread();
[DllImport("kernel32.dll")]
public static extern IntPtr GetCurrentProcess();
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool CloseHandle(IntPtr handle);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool DuplicateHandle(IntPtr hSourceProcessHandle,
IntPtr hSourceHandle, IntPtr hTargetProcessHandle, out IntPtr lpTargetHandle,
uint dwDesiredAccess, bool bInheritHandle, uint dwOptions);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool TerminateThread(IntPtr hThread, uint dwExitCode);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds);
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With