保证取消挂起任务的方法?

时间:2016-08-17 15:48:49

标签: task-parallel-library cancellation thread-abort

我经常需要在一个长时间运行,阻塞,不稳定和\或可能永久挂起的单独线程上执行代码。由于TPL的存在,互联网上充满了使用取消令牌很好地取消任务的示例,但我从未找到过杀死挂起任务的示例。一旦与硬件通信或调用某些第三方代码,就可能会出现永远挂起的代码。挂起的任务无法检查取消令牌,注定永远保持活着。在关键应用中,我为这些任务配备了定期发送的有效信号。一旦检测到挂起任务,它就会被终止并启动一个新实例。

下面的代码显示了一个示例任务,它调用了一个长期运行的占位符方法SomeThirdPartyLongOperation(),它有可能永远挂起。 StopTask()首先检查任务是否仍在运行尝试使用取消令牌取消它。如果这不起作用,则任务挂起并且底层线程被中断\中止旧学校风格。

    private Task _task;
    private Thread _thread;
    private CancellationTokenSource _cancellationTokenSource;

    public void StartTask()
    {
        _cancellationTokenSource = new CancellationTokenSource();
        _task = Task.Factory.StartNew(() => DoWork(_cancellationTokenSource.Token), _cancellationTokenSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
    }

    public void StopTask()
    {
        if (_task.Status == TaskStatus.RanToCompletion)
            return;
        _cancellationTokenSource.Cancel();
        try
        {
            _task.Wait(2000); // Wait for task to end and prevent hanging by timeout.
        }
        catch (AggregateException aggEx)
        {
            List<Exception> exceptions = aggEx.InnerExceptions.Where(e => !(e is TaskCanceledException)).ToList(); // Ignore TaskCanceledException
            foreach (Exception ex in exceptions)
            {
                // Process exception thrown by task
            }
        }
        if (!_task.IsCompleted) // Task hangs and didn't respond to cancellation token => old school thread abort
        {
            _thread.Interrupt();
            if (!_thread.Join(2000))
            { 
                _thread.Abort();
            }
        }
        _cancellationTokenSource.Dispose();
        if (_task.IsCompleted)
        {
            _task.Dispose();
        }
    }

    private void DoWork(CancellationToken cancellationToken)
    {
        if (string.IsNullOrEmpty(Thread.CurrentThread.Name)) // Set thread name for debugging
            Thread.CurrentThread.Name = "DemoThread";
        _thread = Thread.CurrentThread; // Save for interrupting/aborting if thread hangs
        for (int i = 0; i < 10; i++)
        {
            cancellationToken.ThrowIfCancellationRequested();
            SomeThirdPartyLongOperation(i);
        }
    }

虽然我已经使用这种结构多年了,但我想知道它是否存在一些潜在的错误。我从来没有见过一个保存底层线程的任务的例子,或者给它起一个简化调试的名称,所以我有点不确定这是否是正确的方法。欢迎评论任何细节!

2 个答案:

答案 0 :(得分:2)

  

一旦与硬件通信或调用某些第三方代码,就可能会出现永远挂起的代码。

沟通:绝对没有。总有一种方法可以通过通信API进行超时,因此即使硬件行为不当,也无需强行终止I / O操作。

第三方代码:只有你是偏执狂(或者具有24x7自动化等高要求)。

这是底线:

  • 没有办法强行杀死任务。
  • 你可以强制杀死一个线程,但这很容易造成应用程序状态的严重问题,如果在代码的其他部分引入死锁,以及资源泄漏的可能性。
  • 你可以强制杀死一个appdomain,它解决了杀死线程的大部分app状态/死锁问题。但是,它并没有全部解决,而且仍然存在资源泄漏的问题。
  • 您可以强制终止进程。这是唯一真正清洁可靠的解决方案。

因此,如果您选择信任第三方代码,我建议您像任何其他API一样调用它。如果您需要100%的可靠性而不管第三方库,您需要将第三方dll包装到一个单独的进程中,并使用跨进程通信来调用它。

你当前的代码强制杀死线程池线程,这当然不推荐;这些线程属于线程池,而不属于您,即使您指定LongRunning,这仍然是正确的。如果你去kill-thread路由(这不完全可靠),那么我建议使用一个显式线程。

答案 1 :(得分:0)

问题是为什么这个任务甚至都悬而未决?我认为这个问题没有通用的解决办法,但你应该把重点放在总是负责的任务上,而不是强迫中断它。

在这段代码中,您似乎正在寻找一个简单的线程而不是一个任务 - 您不应该将任务链接到线程 - 很可能该任务将切换到另一个在一些异步操作之后进行线程,你将最终杀死一个未连接到你的任务的一个innoccent线程。如果你真的需要杀死整个线程,那就专门为这个工作做一个专门的。

您不应该使用任何用于任务的线程来命名或执行任何操作。默认池。请考虑以下代码:

static void Main(string[] args)
{
    Task.Run(sth);
    Console.Read();
}

static async Task sth()
{
    Thread.CurrentThread.Name = "My name";
    Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
    await Task.Delay(1);
    Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
    Console.WriteLine(Thread.CurrentThread.Name ?? "No name");
}

输出是:

3
4
No name