在.NET Core世界中放弃线程的替代方案是什么?

7

我使用ASP.NET Core 2.2应用程序调用一个第三方库,但该库容易导致CPU占用率达到100%,并且基本上会让机器崩溃——这种情况每月至少发生两次。我无法访问源代码,而且供应商也不会修复问题。

我解决此问题的方法是将这个第三方库隔离在一个.NET Framework 4.x Web服务中,在那里如果我检测到问题,可以通过调用Thread.Abort来停止它。之所以将其隔离在.NET Framework服务中而不是.NET Core中,是因为后者不支持Thread.Abort。当前的解决方案虽然不理想,但已经可以运行。即使知道Thread.Abort可能会导致不稳定(迄今为止还没有出现)。

出于性能考虑,我不希望将库隔离。但到目前为止,我还没有找到一种在.NET Core项目中终止运行线程(或任务)的方法。

有哪些可用的替代方案?


3
只有当您可以访问导致问题的代码时,取消标记才能起作用,例如需要侦听取消操作。而导致系统停顿的问题完全出现在第三方库中。 - AngryHacker
2
@AngryHacker,现在我和你一样变得很好奇,想找到解决这个问题的方法。希望有人能够为我们提供一些有用的信息。 - Maytham Fahmi
5
将其隔离到自己的进程中,然后终止该进程。 - Lasse V. Karlsen
2
@LasseVågsætherKarlsen 这实际上就是我现在正在做的事情 - 不太喜欢这种方法。 - AngryHacker
你尝试过反编译第三方库吗?也许这可以让你修复根本原因。 - Kalten
显示剩余5条评论
2个回答

4
我也同意评论中提到的,在这种情况下关闭整个进程可能是更加干净的解决方案。不过,如果您喜欢使用Thread.Abort方法,至少可以在Windows上使用Win32 Interop调用未受控的TerminateThread API来实现它。下面是一个实现的示例(警告:几乎未经测试)。
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);
    }

}

@AngryHacker,很高兴能帮到你,尽管如果经常使用它,它可能会泄漏内存/资源。为什么您不愿意将其作为进程(而不是线程)进行回收? - noseratio - open to work
1
我遇到的这个问题只是应用程序功能的一小部分。我不想因为这个小部分而重新启动整个过程,从而干扰整个应用程序。 - AngryHacker
1
@AngryHacker,另外请尝试使用ExitThread替代TerminateThread。根据那个淘气的库在其紧密循环内部正在做什么,您可能可以通过类似于QueueUserAPCWH_GETMESSAGE钩子来注入一个要在目标线程上执行的ExitThread调用。 - noseratio - open to work

2
您可以降低Thread.Priority,这在Core 3.0中可用。当没有其他需要时,它仍将使用所有可用的CPU周期,但系统将更加响应。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接