如果可能的话,应该避免中止和中断线程,因为这可能会破坏正在运行的程序的状态。例如,想象一下,如果中止了一个持有资源锁的线程,这些锁将永远不会被释放。
相反,考虑使用信号机制,以便线程可以相互协作,并优雅地处理阻塞和解除阻塞,例如:
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块中调用Abort
或Interrupt
来唤醒一个线程。
不能在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块或受限执行区域。
在中止的情况下,如果睡眠时间不是无限的:
- 调用线程将发出一个
Abort
指令,但在此处阻塞,直到finally块退出,即停在这里,不立即执行Join
语句。
- 被调用线程的状态被设置为
AbortRequested
。
- 被调用线程继续休眠。
- 当被调用线程醒来时,由于其状态为
AbortRequested
,它将继续执行finally块的代码,然后“蒸发”,即退出。
- 当被中止的线程离开finally块时:不会引发异常,不会执行finally块后的代码,并且线程的状态为
Aborted
。
- 调用线程解除阻塞,继续执行
Join
语句,并立即通过,因为被调用线程已经退出。
因此,根据您的无限休眠示例,调用线程将永远阻塞在步骤1。
中断
在中断情况下,如果休眠不是无限的:
文档不是很完善...
- 调用线程将发出一个
中断
信号,并继续执行。
- 调用线程将在
Join
语句上阻塞。
- 被调用线程的状态被设置为在下一个阻塞调用时引发异常,但关键是它在finally块中不会被解除阻塞,即不会被唤醒。
- 被调用线程继续睡眠。
- 当被调用线程醒来时,它将继续执行finally块。
- 当被中断的线程离开finally块时,它将在下一个阻塞调用时抛出一个
ThreadInterruptedException
异常(请参见下面的代码示例)。
- 调用线程"加入"并继续执行,因为被调用线程已经退出,然而,在步骤6中未处理的
ThreadInterruptedException
现在已经扁平化了这个过程...
所以,根据您的无限睡眠示例,调用线程将永远阻塞,但在第2步。
总结
因此,尽管Abort
和Interrupt
的行为略有不同,但它们都会导致被调用的线程永远睡眠,而调用的线程永远阻塞(在您的示例中)。
只有粗鲁的中止才能强制一个被阻塞的线程退出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);
}
}
Interrupt()
作为线程间信号传递机制。有更可预测的API,如Monitor
,{Manual|Auto}ResetEvent
等。 - Marc Gravell