Alternatives to aborting a thread in the .NET Core world?

Viewed 395

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?

2 Answers

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);
    }

}

You could lower the Thread.Priority, which is available in Core 3.0. It'll still use all available CPU cycles when nothing else needs them but the system will be more responsive.

Related