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

Posted

技术标签:

【中文标题】关于在 .NET 中干净地终止线程的问题【英文标题】:Question about terminating a thread cleanly in .NET 【发布时间】:2011-04-07 14:59:28 【问题描述】:

我了解到 Thread.Abort() 从我读过的大量关于该主题的文章中是邪恶的,所以我目前正在删除我所有的中止,以便以更清洁的方式替换它;在比较了人们在 *** 上的用户策略之后,然后在阅读了 MSDN 中的 "How to: Create and Terminate Threads (C# Programming Guide)" 之后,两者都声明了一种非常相同的方法——即使用volatile bool 方法检查策略,这很好,但我还有几个问题......

这里让我印象深刻的是,如果您没有一个简单的工作进程,它只是运行一个处理代码的循环,该怎么办?例如,对我来说,我的进程是一个后台文件上传进程,我实际上是循环遍历每个文件,所以这就是一些东西,我可以肯定我可以在顶部添加我的while (!_shouldStop),它涵盖了我的每次循环迭代,但我有很多在下一次循环迭代之前发生的更多业务流程,我希望这个取消过程快速;不要告诉我我需要在我的整个工作函数中每隔 4-5 行将这些 while 循环洒下来吗?!

我真的希望有更好的方法,如果这实际上是正确的 [也是唯一的?] 方法,或者他们过去为实现我所追求的目标而使用的策略,请告诉我.

谢谢大家。

进一步阅读:All these SO responses 假设工作线程将循环。这让我感到不舒服。如果是线性但及时的后台操作呢?

【问题讨论】:

这样想:你真的希望你的长时间运行的进程在它可能处于某些状态变化处理过程中的任意时间中止吗?您需要在代码中间添加检查的原因(不一定在循环中,尽管这很常见)是因为只有您作为开发人员对您的代码有足够的了解,才能知道何时可以安全停止。如果线程阻塞等待同步信号,请参阅答案中提到的 Interrupt() 方法。 我不否认它不应该由用户(我)处理,它看起来就像很多多余的代码。以某种方式设置诸如尝试、捕获运行、持续监控之类的东西会很好,一旦标志为假,然后可能来自函数的return;。我不知道。 原来我想要的是Thread.Interrupt。虽然我认为至少现在我更喜欢布尔标志检查,以绝对确保我的应用程序状态的完整性。 【参考方案1】:

好吧,不幸的是,在多线程中,您经常不得不为了清洁而牺牲“敏捷性”……如果您Interrupt,您可以立即退出线程,但它不会很干净。所以不,您不必每 4-5 行检查一次_shouldStop,但如果您确实中断了线程,那么您应该处理异常并以干净的方式退出循环。

更新

即使它不是一个循环线程(即,它可能是一个执行一些长时间运行的异步操作或某种类型的输入操作块的线程),你可以Interrupt 它,但你仍然应该抓住ThreadInterruptedException并干净地退出线程。我认为您一直在阅读的示例非常合适。

更新 2.0

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

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

【讨论】:

你有一个如何使用中断的例子吗?谢谢 Lirik。 @GONeale,我现在为您发布了一个示例...如果有帮助,请告诉我。 嗯,这很有道理。我有一种预感,它在任何可能的时候都会引发异常,我们的想法是我们必须抓住它。好的,我会在建议的解决方案之间做出决定,谢谢。【参考方案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;
        ;
    

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

【讨论】:

好的,谢谢。但它确实归结为相同的方法。没问题。【参考方案3】:

所有这些 SO 响应都假定工作线程将循环。这让我感到不舒服

没有很多方法可以让代码花费很长时间。循环是一个非常重要的编程结构。使代码在不循环的情况下花费很长时间需要大量 语句。数十万。

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

【讨论】:

好吧,我没有那么多 :) 是的,在大多数情况下,它可能会包含在我的 for 循环遍历每个文件的顶部,我普遍担心的是一些数据包发送也可以,但我可以在那里重新循环以写入我想的字节。 顺便说一下,在这个 Hans 上,在使用 bool 成功实现了我的检查后,我意识到为什么我最初遇到循环检查问题以及我长时间运行代码的原因,这不是因为很多行,这仅仅是因为某些行调用了远程 WCF 调用的函数,这些调用必须等待响应,这需要时间。【参考方案4】:

您不必到处使用 while 循环。外部 while 循环只是检查它是否被告知停止,如果是,则不会进行另一次迭代......

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

例如:

public void DoWork() 

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


【讨论】:

【参考方案5】:

如果取消是您正在构建的东西的要求,那么它应该像您的代码的其余部分一样受到尊重 - 这可能是您必须设计的东西。

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

    CPU 绑定的东西 等待内核

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

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

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

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

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

    它可以隐藏挂起的后台线程的影响,从而避免因挂起后台进程而导致的潜在隐藏线程/内存泄漏。

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

    您认为自己无法及时中止自己的代码。如果取消是您的应用程序的要求,那么您真正需要做出的决定是资源/业务决定:进行破解或彻底解决您的问题。

    您不信任您正在调用的某些代码,因为它超出了您的控制范围。如果您真的不信任它,请考虑将其移出进程。这样一来,您就可以更好地隔离多种风险,包括这一风险。

【讨论】:

很好地指出取消应该是一个设计决定(如果可能 - 如果您不编写底层代码,则并非总是如此),而不是事后的想法。【参考方案6】:

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

正如您所说,大多数答案都围绕着每几行轮询一个共享布尔值。即使您可能不喜欢它,这通常是最简单的方案。如果你想让你的生活更轻松,你可以编写一个像 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.

>

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

并且做 blucz 说的一切。

【讨论】:

谢谢老兄。感谢您的回复。 “异常不应该用于流控制”的概念基本上意味着如果抛出异常的代码知道它会在哪里被捕获,它应该使用一些其他方式来“到达那里” .取消通常需要输出多层嵌套循环,而内循环代码通常对外循环或如何跳出它们一无所知。【参考方案7】:

不要在不属于循环的地方添加while循环,而是在有意义的地方添加if (_shouldStop) CleanupAndExit();之类的东西。无需在每次操作后检查或将代码撒在上面。相反,应将每次检查视为退出线程的机会,并考虑到这一点,战略性地添加它们。

【讨论】:

【参考方案8】:

很遗憾,可能没有更好的选择。这真的取决于你的具体情况。这个想法是在安全点优雅地停止线程。这就是Thread.Abort不好的症结所在;因为它不能保证在安全点发生。通过在代码中添加停止机制,您可以有效地手动定义安全点。这称为合作取消。基本上有 4 种广泛的机制可以做到这一点。您可以选择最适合您的情况。

投票停止标志

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

使用 TPL 中的新取消机制

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

使用等待句柄

如果您的工作线程需要等待特定时间间隔或在其正常操作期间等待信号,则此方法非常有用。例如,您可以SetManualResetEvent 让线程知道是时候停止了。您可以使用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 的侵入性小,但我仍然不鼓励使用它,因为不清楚哪些调用会响应它,而且许多开发人员会不熟悉它的细微差别。

【讨论】:

信息量很大,并给出了一些很好的例子。非常感谢。 因答案的多样性而获得的积分。谢谢。 如果一个线程将调用一个非托管方法,该方法可能需要很长时间并且应该是可终止的(认识到终止 DLL 可能会使其处于不良状态,但不应影响自身之外的任何东西) ,让线程获取锁并在任何时候除了保持它会是什么效果?获得该锁的外部线程使用Thread.Abort 是否安全,因为要中止的线程不能处于任何不良位置(否则 it 将持有锁)? @supercat:好问题。问题是大多数时候ThreadAbortException 无论如何都不会被注入到线程中,而它处于非托管代码中。我不确定我是否完全理解所说的锁的意义,但我相信我可以推断出你的思维过程……是因为锁会延迟中止注入吗?另外,Hans Passant has some excellent advice for this situation。基本上,唯一安全的方法是在进程外运行非托管代码。 关于Thread.Interrupt() 的一个非常重要的事情在这里没有提到——ThreadInterruptedException 将从任何锁定语句 中抛出或调用有争用的Monitor.Enter()它是因为它被认为是 .NET 中的等待状态。这意味着除非您处理应用程序中每个 lockMonitor.Enter() 语句的错误以及您不拥有的任何正在运行的代码,否则当您调用 Thread.Interrupt() 或抛出异常时处于什么状态。

以上是关于关于在 .NET 中干净地终止线程的问题的主要内容,如果未能解决你的问题,请参考以下文章

在子进程运行和通信时终止子进程,这些子进程通过队列干净地通信

如何干净地停止运行 matlab dll 函数的 .NET 线程

C# WinForm程序中强制退出程序以及启动程序

终止python程序的正确方法,无论调用位置如何,并进行清理

从 Perl 线程中干净地退出

线程 python 应用程序没有干净地关闭