为什么在finally块中睡眠时线程不会被中断?

11

我在 MSDN 上搜寻了很久,但找不到为什么在 finally 块中睡眠的线程无法被中断的原因。我已经尝试过使用 abort 方法,但没有成功。

有没有办法在 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块中可以和不能中断/终止的原因。那就是当进程正在关闭时。 谢谢。


3
我不知道这个问题的答案,但我知道作为一个一般规则,我避免使用 Interrupt() 作为线程间信号传递机制。有更可预测的API,如 Monitor{Manual|Auto}ResetEvent等。 - Marc Gravell
我更想要任何关于在finally中Interrupt/Abort不起作用的解释/文档,以及为什么MSDN说相反的话。我的例子是一种更复杂的轮询器类型,现在我正在阅读建议的waithandles。 - Marek
3
有两种线程中止方式。友好的一种不会打断 finally 块中的代码。不友好的一种是在具有 IsBackground 等于 true 的线程上当进程关闭时调用,或者在出现未处理的异常时终止线程。现在,代码是否被粗暴地中断都无关紧要。 - Hans Passant
@Hans,你的评论也适用于Thread.Interupt吗?它的行为方式是一样的,但我找不到任何文档。 - Tim Lloyd
1
@chiba - CLR不使用Thread.Interrupt来关闭线程。不友好的类型是CLR实现细节。 - Hans Passant
@Hans 对不起,有些误解。我并不是在谈论CLR如何关闭线程,我是在谈论你的评论与“Interupt”的关系。问题是关于“Interupt”,而不是“Abort”。我猜这两件事情是相关的(行为肯定会证明这一点),但我找不到任何文档。 - Tim Lloyd
2个回答

11

如果可能的话,应该避免中止和中断线程,因为这可能会破坏正在运行的程序的状态。例如,想象一下,如果中止了一个持有资源锁的线程,这些锁将永远不会被释放。

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

    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()的记录要少得多。

对于你的问题,简短的答案是不,你不能通过在finally块中调用AbortInterrupt来唤醒一个线程。

不能在finally块中中止或中断线程是有意设计的,这样finally块就有机会按照你的期望运行。如果你能在finally块中中止或中断线程,这可能会对清理程序产生意外后果,从而导致应用程序处于损坏状态 - 这是不好的。

线程中断的一个细微差别是,在进入finally块之前的任何时候,线程可能已经收到了中断请求,但此时它不处于SleepWaitJoin状态(即未阻塞)。在这种情况下,如果finally块中有一个阻塞调用,它会立即抛出ThreadInterruptedException并退出finally块。finally块的保护机制防止了这种情况的发生。

除了在finally块中提供保护外,这也适用于try块和CERs(Constrained Execution Region),可以在用户代码中配置以防止在区域执行之后抛出一系列异常-非常适用于必须完成并延迟中止的关键代码块。
对此的例外是所谓的Rude Aborts。这些是CLR托管环境本身引发的ThreadAbortExceptions。这些异常可能导致finally块和catch块退出,但不会退出CERs。例如,CLR可能会对其判断为执行工作时间过长的线程引发Rude Aborts,例如在尝试卸载AppDomain或在SQL Server CLR中执行代码时。在您的特定示例中,当应用程序关闭并且AppDomain卸载时,CLR将对正在休眠的线程发出Rude Abort,因为存在AppDomain卸载超时。

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

中止

在finally块中调用Abort来中止线程时,调用线程会被阻塞。这在文档中有记录:

调用Abort的线程可能会被阻塞,如果被中止的线程处于受保护的代码区域,例如catch块、finally块或受限执行区域。

在中止的情况下,如果睡眠时间不是无限的:

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

因此,根据您的无限休眠示例,调用线程将永远阻塞在步骤1。

中断

在中断情况下,如果休眠不是无限的:

文档不是很完善...

  1. 调用线程将发出一个中断信号,并继续执行。
  2. 调用线程将在Join语句上阻塞。
  3. 被调用线程的状态被设置为在下一个阻塞调用时引发异常,但关键是它在finally块中不会被解除阻塞,即不会被唤醒。
  4. 被调用线程继续睡眠。
  5. 当被调用线程醒来时,它将继续执行finally块。
  6. 当被中断的线程离开finally块时,它将在下一个阻塞调用时抛出一个ThreadInterruptedException异常(请参见下面的代码示例)。
  7. 调用线程"加入"并继续执行,因为被调用线程已经退出,然而,在步骤6中未处理的ThreadInterruptedException现在已经扁平化了这个过程...

所以,根据您的无限睡眠示例,调用线程将永远阻塞,但在第2步。

总结

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

只有粗鲁的中止才能强制一个被阻塞的线程退出finally块,而这些只能由CLR自身引发(你甚至不能使用反射来修改ThreadAbortException.ExceptionState,因为它会进行内部CLR调用以获取AbortReason - 没有机会轻易地做恶意操作...)。
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
    }
}   

5

finally块的整个意义在于它保存的内容不会受到中断或中止的影响,无论发生什么情况都会正常完成运行。如果允许finally块被中断或中止,那么这基本上就失去了它的意义。遗憾的是,正如你所指出的,因为各种竞争条件,finally块可能会被中断或中止。这就是为什么许多人建议您不要中断或中止线程。

相反,使用协作式设计。如果线程需要被中断,而不是调用Sleep,请使用定时等待。不要调用Interrupt,而是发出线程等待的信号。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接