关于在.NET中干净地终止线程的问题

时间:2010-09-03 00:00:03

标签: c# .net winforms multithreading

我理解Thread.Abort()在我读过的关于这个主题的大量文章中是邪恶的,所以我现在正在扯掉我的中止,以便以更清洁的方式取代它;并且在比较了stackoverflow上的人们的用户策略之后,然后在阅读了来自MSDN的How to: Create and Terminate Threads (C# Programming Guide)之后,两者都表示方法非常相似 - 即使用{{1}接近检查策略,这很好,但我还有几个问题......

如果你没有一个简单的工作进程只是运行一个运算循环的代码,那么这里有什么突出的意义呢?比如说对我来说,我的进程是一个后台文件上传程序进程,我实际上是循环遍历每个文件,所以这是一些东西,并确保我可以在顶部添加我的volatile bool,它涵盖了我每次循环迭代,但我有更多的业务流程在它下一次循环迭代之前发生,我希望这个取消过程是快速的;不要告诉我,我需要在整个工作人员功能中每隔4-5行循环播放这些内容吗?!

我真的希望有一个更好的方法,有人可以告诉我这是否实际上是正确的[并且只有?]做到这一点的方法,或者他们过去用来实现我所追求的目标的策略

感谢帮派。

进一步阅读:All these SO responses假设工作线程将循环。这并不适合我。如果它是线性的,但是及时的背景操作怎么办?

8 个答案:

答案 0 :(得分:101)

不幸的是,可能没有更好的选择。这实际上取决于您的具体情况。我们的想法是在安全点优雅地停止线程。这就是Thread.Abort不好的原因所在;因为不能保证在安全点发生。通过使用停止机制喷洒代码,您可以有效地手动定义安全点。这称为合作取消。这基本上有4种广泛的机制。您可以选择最适合您情况的那个。

轮询停止标志

您已经提到过这种方法。这是一个很常见的。定期检查算法中安全点的标志,并在发出信号时进行挽救。标准方法是标记变量volatile。如果这不可能或不方便,那么您可以使用lock。请记住,您不能将局部变量标记为volatile,因此,例如,如果lambda表达式通过闭包捕获它,那么您将不得不求助于另一种方法来创建所需的内存屏障。这种方法不需要说太多其他内容。

使用TPL中的新取消机制

这类似于轮询停止标志,除了它使用TPL中的新取消数据结构。它仍然基于合作取消模式。您需要获得CancellationToken并定期检查IsCancellationRequested。要请求取消,您可以在最初提供令牌的Cancel上致电CancellationTokenSource。新的取消机制可以做很多事情。您可以阅读有关here的更多信息。

使用等待句柄

如果您的工作线程需要在特定时间间隔内等待或在正常操作期间发出信号,则此方法非常有用。例如,您可以Set ManualResetEvent让线程知道是时候停止了。您可以使用WaitOne函数测试事件,该函数返回bool,指示事件是否已发出信号。 WaitOne接受一个参数,该参数指定如果事件未在该时间内发出信号,则等待调用返回的时间。您可以使用此技术代替Thread.Sleep并同时获取停止指示。如果线程可能必须等待的其他WaitHandle个实例,它也很有用。您可以在一次通话中呼叫WaitHandle.WaitAny等待任何事件(包括停止事件)。使用事件可能比调用Thread.Interrupt更好,因为您可以更好地控制程序流(Thread.Interrupt会抛出异常,因此您必须策略性地放置try-catch块来执行任何必要的清理)。

专业方案

有几种一次性场景具有非常专业的停止机制。绝对超出了这个答案的范围,列举所有这些(更别提它几乎是不可能的)。我的意思是Socket类。如果线程在调用SendReceive时被阻止,那么调用Close将在有效解除阻塞的任何阻塞调用中中断套接字。我相信在BCL中还有其他几个领域可以使用类似的技术来解锁一个线程。

通过Thread.Interrupt

中断线程

这里的优点是它很简单,你不必专注于用任何东西真正地洒你的代码。缺点是您几乎无法控制算法中安全点的位置。原因是因为Thread.Interrupt通过在一个固定的BCL阻塞调用中注入异常来工作。其中包括Thread.SleepWaitHandle.WaitOneThread.Join等。因此,您必须明智地放置它们。但是,大多数情况下,算法决定了它们的去向,而且通常情况下通常都很好,特别是如果你的算法大部分时间花在这些阻塞调用之一上。如果算法没有使用BCL中的一个阻塞调用,那么此方法将不适合您。这里的理论是ThreadInterruptException仅从.NET等待调用生成,因此它可能可能处于安全点。至少你知道线程不能处于非托管代码中,也不能脱离关键部分,从而在获得状态下留下悬空锁。尽管这比Thread.Abort的侵入性更小,但我仍然不鼓励使用它,因为不明显哪些呼叫响应它,许多开发人员将不熟悉它的细微差别。

答案 1 :(得分:12)

嗯,不幸的是,在多线程中,你经常不得不为了清洁而妥协“快速”...如果你Interrupt它可以立即退出线程,但它不会很干净。所以不,你不必每4-5行都要进行_shouldStop检查,但如果你确实打断了你的线程,那么你应该处理异常并以干净的方式退出循环。

更新

即使它不是一个循环线程(也许它是一个线程执行一些长时间运行的异步操作或某种类型的块用于输入操作),你可以Interrupt它,但你仍然应该抓住{ {1}}并彻底退出线程。我认为你读过的例子非常合适。

更新2.0

是的我有一个例子......我只会根据您引用的链接向您展示一个示例:

ThreadInterruptedException

答案 2 :(得分:8)

如果取消是您正在构建的东西的要求,那么它应该得到与其他代码一样多的尊重 - 它可能是您必须设计的东西。

让我们假设你的线程一直在做两件事之一。

  1. CPU绑定的东西
  2. 等待内核
  3. 如果您在相关线程中受到CPU限制,那么可能有一个很好的位置来插入纾困支票。如果您正在调用其他人的代码来执行一些长时间运行的CPU绑定任务,那么您可能需要修复外部代码,将其移出进程(中止线程是邪恶的,但是中止进程是明确定义且安全的)等等。

    如果你正在等待内核,那么等待中可能还有一个句柄(或fd或mach端口......)。通常,如果您销毁相关句柄,内核将立即返回一些失败代码。如果你在.net / java / etc。你最终可能会遇到异常。在C中,无论您已经处理系统调用失败的代码,都会将错误传播到应用程序的有意义部分。无论哪种方式,你都可以非常及时地打破低级别的地方,而不需要在任何地方洒上新代码。

    我经常使用这种代码的策略是跟踪需要关闭的句柄列表,然后让我的中止函数设置“已取消”标志然后关闭它们。当函数失败时,它可以检查标志并报告由于取消而导致的失败,而不是由于特定异常/错误是什么。

    您似乎暗示取消的可接受粒度是在服务调用级别。这可能不是一个好主意 - 你最好同步取消后台工作并从前台线程加入旧的后台线程。它更清洁,因为:

    1. 当旧的bgwork线程在意外延迟后恢复生机时,它避免了一类竞争条件。

    2. 通过使挂起的后台线程的效果隐藏起来,可以避免因挂起后台进程而导致潜在的隐藏线程/内存泄漏。

    3. 害怕这种方法有两个原因:

      1. 您认为不能及时中止自己的代码。如果取消是您的应用程序的要求,您真正需要做出的决定是资源/业务决策:做一个黑客,或干净地解决您的问题。

      2. 您不相信您正在调用的某些代码,因为它不受您的控制。如果您真的不信任它,请考虑将其移出进程外。你可以更好地隔离多种风险,包括这种风险。

答案 3 :(得分:2)

问题的一部分可能就是你有这么长的方法/ while循环。无论您是否遇到线程问题,都应将其分解为更小的处理步骤。假设这些步骤是Alpha(),Bravo(),Charlie()和Delta()。

然后你可以这样做:

    public void MyBigBackgroundTask()
    {
        Action[] tasks = new Action[] { Alpha, Bravo, Charlie, Delta };
        int workStepSize = 0;
        while (!_shouldStop)
        {
            tasks[workStepSize++]();
            workStepSize %= tasks.Length;
        };
    }

所以是的,它无休止地循环,但检查是否是时候在每个业务步骤之间停止。

答案 4 :(得分:2)

你不必随处撒上while循环。外部while循环只检查它是否被告知停止,如果是,则不再进行另一次迭代...

如果您有直接的“go do something and close out”线程(其中没有循环),那么您只需在线程内的每个主要点之前或之后检查_shouldStop boolean。这样你就知道它是应该继续还是拯救。

例如:

public void DoWork() {

  RunSomeBigMethod();
  if (_shouldStop){ return; }
  RunSomeOtherBigMethod();
  if (_shouldStop){ return; }
  //....
}

答案 5 :(得分:2)

最佳答案在很大程度上取决于你在线程中所做的事情。

  • 就像你说的那样,大多数答案都围绕着每一对线路轮询一个共享的布尔值。即使你可能不喜欢它,这通常是最简单的方案。如果你想让你的生活更轻松,你可以写一个像ThrowIfCancelled()这样的方法,如果你完成了它会抛出某种异常。纯粹主义者会说这是(喘息)使用控制流的例外,但是再次加速是非常的imo。

  • 如果您正在进行IO操作(如网络内容),您可能需要考虑使用异步操作执行所有操作。

  • 如果您正在执行一系列步骤,则可以使用IEnumerable技巧制作状态机。示例:

<

abstract class StateMachine : IDisposable
{
    public abstract IEnumerable<object> Main();

    public virtual void Dispose()
    {
        /// ... override with free-ing code ...
   }

   bool wasCancelled;

   public bool Cancel()
   {
     // ... set wasCancelled using locking scheme of choice ...
    }

   public Thread Run()
   {
       var thread = new Thread(() =>
           {
              try
              {
                if(wasCancelled) return;
                foreach(var x in Main())
                {
                    if(wasCancelled) return;
                }
              }
              finally { Dispose(); }
           });
       thread.Start()
   }
}

class MyStateMachine : StateMachine
{
   public override IEnumerabl<object> Main()
   {
       DoSomething();
       yield return null;
       DoSomethingElse();
       yield return null;
   }
}

// then call new MyStateMachine().Run() to run.

&GT;

过度设计?这取决于您使用的状态机数量。如果你只有1,是的。如果你有100,那么也许不是。太棘手了?这得看情况。这种方法的另一个好处是,它允许你(稍作修改)将你的操作移动到Timer.tick回调中,如果有意义的话,完全无效线程。

并做出blucz所说的一切。

答案 6 :(得分:2)

不要在循环不属于的地方添加while循环,而是在有意义的地方添加类似if (_shouldStop) CleanupAndExit();的内容。无需在每次操作后进行检查,也无需随身携带代码。相反,将每个检查视为在此时退出线程的机会,并在策略上添加它们,并考​​虑到这一点。

答案 7 :(得分:1)

  

所有这些SO响应都假定工作线程将循环。这并不适合我

没有很多方法可以让代码花费很长时间。循环是一个非常重要的编程结构。使代码花费很长时间而不进行循环需要巨大的数量的语句。成千上万。

或者调用一些正在为您循环的其他代码。是的,很难让代码停止按需。那是行不通的。