代码之家  ›  专栏  ›  技术社区  ›  GONeale

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

  •  57
  • GONeale  · 技术社区  · 16 年前

    我了解thread.abort()是一个邪恶的话题,因为我读过很多关于这个话题的文章,所以我现在正在从abort中翻出一个更干净的方法来替换它;在比较了stackoverflow上的用户策略之后,在阅读之后 “ How to: Create and Terminate Threads (C# Programming Guide) “ 从msdn这两个状态的方法非常相同——这是使用 volatile bool 接近检查策略,这很好,但我还有几个问题……

    在这里,我马上会发现,如果您没有一个简单的工作进程,它只是运行一个处理代码的循环,该怎么办?例如,对于我来说,我的过程是一个后台文件上传程序过程,实际上我会循环遍历每个文件,所以这是一个问题,我可以添加我的 while (!_shouldStop) 在顶部,它覆盖了我的每个循环迭代,但是在它进入下一个循环迭代之前,我有更多的业务流程,我希望这个取消过程是快速的;不要告诉我需要在整个工作程序中每4-5行循环一次的同时喷洒这些流程?!

    我真的希望有一个更好的方法,如果这是真的,有人能给我提个建议吗?只有这样才是正确的。]这样做的方法,或者他们过去用来实现我追求的目标的策略。

    谢谢帮会。

    进一步阅读: All these SO responses 假设工作线程将循环。我坐得不舒服。如果是一个线性的,但及时的背景操作呢?

    8 回复  |  直到 9 年前
        1
  •  100
  •   Brian Gideon    9 年前

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

    轮询停止标志

    您已经提到了这个方法。这是很普通的一个。定期检查算法中安全点处的标志,并在收到信号时退出。标准方法是标记变量 volatile . 如果这不可能或不方便,则可以使用 lock . 记住,不能将局部变量标记为 不稳定的 因此,例如,如果lambda表达式通过一个闭包捕获它,那么您必须使用另一种方法来创建所需的内存屏障。对于这个方法,没有其他很多东西需要说。

    在TPL中使用新的取消机制

    这与轮询停止标志类似,只是它在TPL中使用了新的取消数据结构。它仍然基于合作取消模式。你需要一个 CancellationToken 以及定期检查 IsCancellationRequested . 要申请取消,你可以打电话 Cancel 上 CancellationTokenSource 最初提供令牌的。对于新的取消机制,您可以做很多事情。你可以阅读更多关于 here .

    使用等待句柄

    如果工作线程在正常操作期间需要等待特定的间隔或信号,则此方法非常有用。你可以 Set 一 ManualResetEvent 例如,让线程知道该停止了。您可以使用 WaitOne 返回一个 bool 指示事件是否已发出信号。这个 侍者 接受一个参数,该参数指定如果在该时间段内未发出事件信号,则等待调用返回的时间。你可以用这种方法代替 Thread.Sleep 同时得到停车指示。如果还有其他的 WaitHandle 线程可能必须等待的实例。你可以打电话 WaitHandle.WaitAny 在一个调用中等待任何事件(包括停止事件)。使用事件可能比调用 Thread.Interrupt 因为你对程序的流程有更多的控制( 线程中断 抛出一个异常,这样您就必须战略性地将 try-catch 阻止执行任何必要的清理)。

    专用场景

    有几种一次性方案具有非常专门的停止机制。把它们全部列举出来肯定是不在这个答案的范围之内的(不要介意这几乎是不可能的)。一个很好的例子就是 Socket 班级。如果线程在调用时被阻塞 Send 或 Receive 然后打电话 Close 将中断任何阻塞调用上的套接字,从而有效地解除阻塞。我确信BCL中还有其他几个领域可以使用类似的技术来解除线程阻塞。

    通过中断线程 线程中断

    这里的优点是它很简单,并且您不必将精力集中在用任何真正的东西来喷洒代码上。缺点是你对算法中的安全点几乎没有控制权。原因是因为 线程中断 通过在一个封闭的BCL阻塞调用中插入异常来工作。这些包括 线程睡眠 , WaitHandle.WaitOne , Thread.Join 等等,所以你必须知道你把它们放在哪里。但是,大多数时间算法都会指示它们的去向,这通常都很好,尤其是当您的算法大部分时间都花在这些阻塞调用中时。如果您的算法不使用BCL中的一个阻塞调用,那么这个方法将不适用于您。这里的理论是 ThreadInterruptException 仅从.NET等待调用生成,因此 可能的 在安全的地方。至少您知道线程不能在非托管代码中,也不能从关键部分中退出,从而使挂起的锁处于获取状态。尽管这比 线程中止 我仍然不鼓励使用它,因为不清楚哪个调用响应它,而且许多开发人员将不熟悉它的细微差别。

        2
  •  12
  •   Kiril    16 年前

    嗯,不幸的是,在多线程操作中,你经常不得不牺牲“快速性”来保证清洁…如果您 Interrupt 但不会很干净。所以不,你不必洒 _shouldStop 每4-5行检查一次,但是如果您确实中断了线程,那么您应该处理异常并以一种干净的方式退出循环。

    更新

    即使它不是一个循环线程(也就是说,它可能是执行一些长时间运行的异步操作的线程,或者是执行某种类型的输入操作的块),您也可以 中断 但你还是应该抓住 ThreadInterruptedException 然后将线干净地退出。我认为你读过的例子非常合适。

    更新2

    是的,我有一个例子…我将根据您引用的链接向您展示一个示例:

    public class InterruptExample
    {
        private Thread t;
        private volatile boolean alive;
    
        public InterruptExample()
        {
            alive = false;
    
            t = new Thread(()=>
            {
                try
                {
                    while (alive)
                    {
                        /* Do work. */
                    }
                }
                catch (ThreadInterruptedException exception)
                {
                    /* Clean up. */
                }
            });
            t.IsBackground = true;
        }
    
        public void Start()
        {
            alive = true;
            t.Start();
        }
    
    
        public void Kill(int timeout = 0)
        {
            // somebody tells you to stop the thread
            t.Interrupt();
    
            // Optionally you can block the caller
            // by making them wait until the thread exits.
            // If they leave the default timeout, 
            // then they will not wait at all
            t.Join(timeout);
        }
    }
    
        3
  •  8
  •   blucz    16 年前

    如果取消是您正在构建的东西的一个需求,那么应该像对待其他代码一样尊重它——它可能是您必须为之设计的东西。

    假设线程一直在做两件事之一。

    1. CPU限制的东西
    2. 等待内核

    如果您在所讨论的线程中受到CPU限制,那么 可能 找个好地方插入紧急援助支票。如果您调用其他人的代码来执行一些长时间运行的CPU绑定任务,那么您可能需要修复外部代码,将其移出进程(中止线程是邪恶的,但中止进程是定义良好且安全的)等。

    如果您正在等待内核,那么等待中可能涉及到一个句柄(或者fd,或者mach端口,…)。通常,如果销毁相关的句柄,内核将立即返回一些失败代码。如果你在.NET/Java/等中,你很可能会遇到一个例外。在C语言中,无论您已经准备好处理系统调用失败的代码是什么,都会将错误传播到应用程序中有意义的部分。无论哪种方式,您都可以相当干净、非常及时地从低级的地方脱离出来,而不需要到处撒新的代码。

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

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

    1. 它避免了当旧的bgwork线程在意外延迟后恢复正常时出现的一类争用条件。

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

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

    1. 您认为不能及时中止自己的代码。如果取消是应用程序的一项要求,那么您真正需要做的决定是一个资源/业务决定:进行黑客攻击,或者彻底解决您的问题。

    2. 你不信任你正在调用的代码,因为它超出了你的控制范围。如果你真的不信任它,考虑把它从过程中移开。通过这种方式,您可以更好地隔离各种风险,包括这种风险。

        4
  •  2
  •   Brent Arias    16 年前

    也许问题的一点是您有这么长的方法/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;
            };
        }
    

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

        5
  •  2
  •   ChrisLively    16 年前

    你不必在到处都是水圈的时候洒水。外部while循环只是检查它是否被告知停止,如果停止,就不会进行另一次迭代…

    如果你有一个直接的“去做一些事情并关闭”线程(其中没有循环),那么你只需要在线程中的每个主要点之前或之后检查“shouldstop”布尔值。这样你就知道它是应该继续下去还是应该退出。

    例如:

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

        6
  •  2
  •   Glenn    16 年前

    最好的答案很大程度上取决于你在做什么。

    • 正如您所说,大多数答案都围绕着每两行轮询一个共享的布尔值。尽管你可能不喜欢它,但这通常是最简单的方案。如果你想让你的生活更轻松,你可以写一个类似throwifcancelled()的方法,如果你完成了,它会抛出某种异常。纯粹主义者会说,这是(喘息)使用异常控制流,但再次Cacelling是异常的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.
    

    >

    过度工程?这取决于您使用多少状态机。如果你只有一个,是的。如果你有100个,那么也许没有。太狡猾了吗?嗯,要看情况。这种方法的另一个好处是,它允许您(稍作修改)将操作移动到timer.tick回调中,如果有意义的话,还可以完全取消线程。

    做布卢茨说的一切。

        7
  •  2
  •   Adrian Lopez    16 年前

    不要在循环不属于的地方添加while循环,而是添加类似 if (_shouldStop) CleanupAndExit(); 在任何有意义的地方。不需要在每次操作后都进行检查,也不需要在代码上撒满它们。相反,把每一个检查都看作是一个在那一点上退出线程的机会,并在战略上记住这一点。

        8
  •  1
  •   Hans Passant    16 年前

    所有这些响应都假定工作线程将循环。我坐得不舒服

    没有很多方法可以让代码花费很长时间。循环是一个非常重要的编程结构。使代码长时间不循环需要 巨大的 报表金额。几十万。

    或者调用其他为您执行循环的代码。是的,很难让代码按需停止。那不管用。