【问题标题】:Why is thread not interrupted when sleeping in finally block为什么在finally块中休眠时线程不中断
【发布时间】:2011-10-27 05:55:44
【问题描述】:

我一直在寻找 MSDN 并找不到在 finally 块内休眠时不能中断线程的原因。我试过中止但没有成功。

有什么办法可以在 finally 块内休眠时唤醒线程?

Thread t = new Thread(ProcessSomething) {IsBackground = false};
t.Start();
Thread.Sleep(500);
t.Interrupt();
t.Join();

private static void ProcessSomething()
{
    try { Console.WriteLine("processing"); }
    finally
    {
        try
        {
            Thread.Sleep(Timeout.Infinite);
        }
        catch (ThreadInterruptedException ex)
        {
            Console.WriteLine(ex.Message);
        }
    }
}

令人惊讶的是,MSDN 声称线程可以在 finally 块中中止:http://msdn.microsoft.com/en-us/library/aa332364(v=vs.71).aspx “线程有可能在 finally 块运行时中止,在这种情况下 finally 块被中止。”

编辑 我发现 Hans Passant 评论是最佳答案,因为这解释了为什么 Thread 有时可以并且不能在 finally 块中被中断/中止。那就是进程关闭的时候。 谢谢

【问题讨论】:

  • 我不知道这个问题的答案,但我确实知道,作为一般规则,我避免将Interrupt() 作为线程间信号传递的机制。还有更多可预测的 API - Monitor{Manual|Auto}ResetEvent
  • 当中断/中止最终不起作用时,我更多地寻找任何解释/文档,为什么 MSDN 说相反。我的示例是更复杂的轮询器类型,我现在正在按照建议将其阅读到等待句柄。
  • 线程中止有两种。友好的人不会在 finally 块中中断代码。当进程在 IsBackground 等于 true 的线程上关闭或在未处理的异常上终止时,将调用不友好的进程。现在,代码被粗暴地打断并不重要。
  • @Hans 您的评论是否也适用于 Thread.Interupt?它的行为方式确实如此,但找不到任何文档。
  • @chiba - CLR 不使用 Thread.Interrupt 来关闭线程。不友好的类型是 CLR 实现细节。

标签: c# multithreading interrupted-exception


【解决方案1】:

finally 块的全部意义在于保存不受中断或中止影响的东西,并且无论如何都会正常完成。允许finally 块被中止或中断几乎会破坏这一点。遗憾的是,正如您所指出的,finally 块可能会由于各种竞争条件而中止或中断。这就是为什么您会看到很多人建议您不要中断或中止线程。

相反,使用合作设计。如果一个线程应该被中断,而不是调用Sleep,使用定时等待。而不是调用Interrupt 发出线程等待的信号。

【讨论】:

    【解决方案2】:

    应尽可能避免中止和中断线程,因为这会破坏正在运行的程序的状态。例如,假设您中止了一个持有对资源开放的锁的线程,这些锁将永远不会被释放。

    请考虑使用信号机制,以便线程可以相互协作,从而优雅地处理阻塞和解除阻塞,例如:

        private readonly AutoResetEvent ProcessEvent = new AutoResetEvent(false);
        private readonly AutoResetEvent WakeEvent = new AutoResetEvent(false);
    
        public void Do()
        {
            Thread th1 = new Thread(ProcessSomething);
            th1.IsBackground = false;
            th1.Start();
    
            ProcessEvent.WaitOne();
    
            Console.WriteLine("Processing started...");
    
            Thread th2 = new Thread(() => WakeEvent.Set());
            th2.Start();
    
            th1.Join();
            Console.WriteLine("Joined");
        }
    
        private void ProcessSomething()
        {
            try
            {
                Console.WriteLine("Processing...");
                ProcessEvent.Set();
            }
            finally
            {
                WakeEvent.WaitOne();
                Console.WriteLine("Woken up...");
            }
        }
    

    更新

    相当有趣的低级问题。虽然Abort() 已记录在案,但Interrupt() 却少得多。

    您的问题的简短回答是否定的,您不能通过调用 AbortInterrupt 来唤醒 finally 块中的线程。

    无法在 finally 块中中止或中断线程是设计使然,只是为了让 finally 块有机会按您的预期运行。如果您可以在 finally 块中中止和中断线程,这可能会对清理例程产生意想不到的后果,从而使应用程序处于损坏状态 - 不好。

    线程中断的一个细微差别是,在线程进入 finally 块之前的任何时候,可能已经针对线程发出了中断,但此时它并未处于 SleepWaitJoin 状态(即未阻塞)。在这种情况下,如果 finally 块中有阻塞调用,它将立即抛出 ThreadInterruptedException 并从 finally 块中崩溃。最后,块保护可以防止这种情况发生。

    除了 finally 块中的保护外,这还扩展到 try 块和 CER (Constrained Execution Region),可以在用户代码中配置,以防止在执行区域之前引发一系列异常 - 对于关键问题非常有用必须完成并延迟中止的代码块。

    这个例外(没有双关语)是所谓的Rude Aborts。这些是 CLR 托管环境本身提出的ThreadAbortExceptions。这些可能会导致退出 finally 和 catch 块,但不会 CER。例如,CLR 可能会引发 Rude Aborts 以响应它判断为花费太长时间才能完成工作的线程\退出,例如在尝试卸载 AppDomain 或在 SQL Server CLR 中执行代码时。在您的特定示例中,当您的应用程序关闭并且 AppDomain 卸载时,CLR 将在睡眠线程上发出粗鲁中止,因为会有 AppDomain 卸载超时。

    finally 块中的中止和中断不会在用户代码中发生,但两种情况之间的行为略有不同。

    中止

    当在 finally 块中的线程上调用 Abort 时,调用线程被阻塞。这是documented

    如果正在中止的线程位于代码的受保护区域(例如 catch 块、finally 块或受约束的执行区域)中,则调用 Abort 的线程可能会阻塞。

    如果睡眠不是无限期的中止情况:

    1. 调用线程将发出一个Abort,但在此处阻塞,直到退出finally 块,即它在此处停止并且不会立即执行Join 语句。
    2. 被调用线程的状态设置为AbortRequested
    3. 被调用者继续休眠。
    4. 当被调用者醒来时,由于其状态为AbortRequested,它将继续执行 finally 块代码,然后“蒸发”即退出。
    5. 当被中止的线程离开finally块时:不引发异常,finally块后没有代码执行,线程状态为Aborted
    6. 调用线程已解除阻塞,继续执行Join 语句,并在被调用线程退出时立即通过。

    因此,鉴于您的示例无限睡眠,调用线程将在第 1 步永远阻塞。

    中断

    如果睡眠不是无限期的中断情况:

    没有很好的记录...

    1. 调用线程将发出Interrupt继续执行。
    2. 调用线程将阻塞Join 语句。
    3. 被调用线程的状态设置为在下一次阻塞调用时引发异常,但至关重要的是,由于它位于 finally 块中,因此它没有被解除阻塞,即被唤醒。
    4. 被调用者继续休眠。
    5. 当被调用者醒来时,它会继续执行 finally 块。
    6. 当被中断的线程离开 finally 块时,它将在下一次阻塞调用时抛出 ThreadInterruptedException(参见下面的代码示例)。
    7. 调用线程“加入”并在被调用线程退出时继续,但是,步骤 6 中未处理的 ThreadInterruptedException 现在已使进程变平...

    再次给出您的无限睡眠示例,调用线程将永远阻塞,但在第 2 步。

    总结

    因此,尽管 AbortInterrupt 的行为略有不同,但它们都会导致被调用线程永远休眠,并且调用线程永远阻塞(在您的示例中)。

    只有粗鲁的 Abort 可以强制阻塞的线程退出 finally 块,并且这些只能由 CLR 本身引发(你甚至不能使用反射来欺骗 ThreadAbortException.ExceptionState,因为它会进行内部 CLR 调用以获取 @ 987654346@ - 那里没有机会轻易作恶...)。

    CLR 可防止用户代码导致 finally 块过早退出,这有助于防止损坏状态。

    举个与Interrupt略有不同的行为示例:

    internal class ThreadInterruptFinally
    {
        public static void Do()
        {
            Thread t = new Thread(ProcessSomething) { IsBackground = false };
            t.Start();
            Thread.Sleep(500);
            t.Interrupt();
            t.Join();
        }
    
        private static void ProcessSomething()
        {
            try
            {
                Console.WriteLine("processing");
            }
            finally
            {
                Thread.Sleep(2 * 1000);
            }
    
            Console.WriteLine("Exited finally...");
    
            Thread.Sleep(0); //<-- ThreadInterruptedException
        }
    }   
    

    【讨论】:

      猜你喜欢
      • 2015-10-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-01-18
      相关资源
      最近更新 更多