C#的using语句是否具备中止安全性?

30

我刚刚读完了《C# 4.0 in a Nutshell》(O'Reilly出版)这本书,我认为它是一本非常适合想转换到C#的程序员的好书,但是它让我感到困惑。我的问题是using语句的定义。根据这本书(第138页),

using (StreamReader reader = File.OpenText("file.txt")) {
    ...
}

精确等价于:

StreamReader reader = File.OpenText("file.txt");
try {
    ...
} finally {
    if (reader != null)
        ((IDisposable)reader).Dispose();
}

假设这是真的,而且这段代码在一个单独的线程中被执行。这个线程现在被 thread.Abort() 中止,因此抛出了一个 ThreadAbortException,并且假设线程在初始化读取器之后、进入 try..finally 子句之前正好停止。这意味着读取器没有被处理!

一种可能的解决方案是这样编码:

StreamReader reader = null;
try {
    reader = File.OpenText("file.txt");
    ...
} finally {
    if (reader != null)
        ((IDisposable)reader).Dispose();
}

这将是“可中止安全”的。

现在我的问题是:

  1. 书的作者是否正确,using语句不是可中止安全的,还是他们错了,它的行为类似于我的第二个解决方案?
  2. 如果using等同于第一种情况(不可中止安全),为什么要在finally中检查null
  3. 根据该书(第856页)的说明,ThreadAbortException可以在托管代码中的任何地方抛出。但也许有例外情况,第一种情况毕竟是可中止安全的吗?

编辑:我知道使用thread.Abort()不被视为良好的实践。我感兴趣的纯粹是理论上的: using语句的确切行为是什么?


如果在Dispose调用之前的finally块中出现ThreadAbortException,会发生什么? - liori
14
调用thread.Abort方法的行为就像怀疑汽车气囊是否真正起作用一样,如果你需要知道,那么你已经有了更大的问题。请注意,本翻译力求通俗易懂,且未改变原意。 - Marc Gravell
确切地说……您可能会想出一百万种不同的时间,发生糟糕的事情。基本上,ThreadAbort是表明已经发生了非常可怕的事情的迹象。在那一点上,未处理的FileReader是最不用担心的问题。 - Clyde
根据Joe Albahari的说法(请参见他下面的回答),所有的catch/finally块都会被执行,并且不会在中途被中止。 - Dzinx
9个回答

18
书籍的相关网站提供了有关中止线程的更多信息,请点击此处
简而言之,第一种翻译是正确的(你可以通过查看IL来确定)。
至于你的第二个问题的答案是,可能存在变量可以合法地为null的情况。例如,GetFoo() 在此处可能返回 null,因此您不希望在隐式finally块中抛出NullReferenceException:
using (var x = GetFoo())
{
   ...
}
回答你的第三个问题,如果你正在调用框架代码,使 Abort 变得安全的唯一方法是在之后拆除 AppDomain。在许多情况下,这实际上是一个实用的解决方案(这正是 LINQPad 在取消运行查询时所做的)。

很棒的文章,这正是我在寻找的。而且还是来自作者本人!谢谢! - Dzinx

9
你的两种情况实际上没有区别,即使在第二种情况下,在调用OpenText之后,ThreadAbort仍然可能发生,但在结果分配给reader之前。基本上,当你遇到ThreadAbortException时,所有的筹码都失去了作用。这就是为什么你永远不应该故意中止线程而使用其他方法优雅地关闭线程的原因。
针对你的编辑--我再次指出,你的两种情况实际上是相同的。除非File.OpenText调用成功并返回一个值,否则'reader'变量将为null,因此编写代码的第一种方式与第二种方式之间没有区别。

7
Thread.Abort非常危险;如果人们正在调用它,那么你已经遇到了很多麻烦(不可恢复的锁等)。Thread.Abort应该只在处理不健康的进程时使用。
异常通常可以干净地解开,但在极端情况下,不能保证每一行代码都能执行。更加紧迫的例子是“如果电源失败会发生什么”。
对于null检查,如果File.OpenText返回null怎么办?好吧,它不会,但是编译器不知道这一点。

+1 对于“null”的解释:你说得对,不仅构造函数可以在“using”语句中使用。谢谢! - Dzinx
@DzinX - 同样,构造函数也可以返回null。不仅适用于Nullable<T>,而且对于类也是如此。是的,真的。 - Marc Gravell
请点击此处查看第二部分(“情节渐浓”)。 - Marc Gravell

4

有些离题,但锁定语句在线程中止时的行为也很有趣。虽然 lock 等效于:

object obj = x;
System.Threading.Monitor.Enter(obj);
try {
    …
}
finally {
    System.Threading.Monitor.Exit(obj);
}

由x86 JITter保证,在Monitor.Enter和try语句之间不会发生线程中止。
http://blogs.msdn.com/b/ericlippert/archive/2007/08/17/subtleties-of-c-il-codegen.aspx 在.NET 4中,生成的IL代码似乎有所不同:
http://blogs.msdn.com/b/ericlippert/archive/2009/03/06/locks-and-exceptions-do-not-mix.aspx

有趣。你有任何引用可以证明吗?我在语言规范中找不到它。 - Dzinx
@dzinx @codeinchaos - 请查看Eric Lippert的博客。它已经在待定规范版本中更新(如果尚未更新)。此外,事件也进行了全面改进。 - Marc Gravell

2
作者是正确的。使用语句块不是中止安全的。您的第二个解决方案也不是中止安全的,线程可能在资源获取的中间被中止。
虽然它不是中止安全的,但任何具有未管理资源的可处置对象也应该实现终结器,这将最终运行并清理资源。终结器应该足够健壮,以处理未完全初始化的对象,以防线程在资源获取的中间中止。
Thread.Abort仅会等待在约束执行区域(CER)、finally块、catch块、静态构造函数和未管理代码内运行的代码。因此,这是一个中止安全的解决方案(仅涉及资源的获取和处理):
StreamReader reader = null;
try {
  try { }
  finally { reader = File.OpenText("file.txt"); }
  // ...
}
finally {
  if (reader != null) reader.Dispose();
}

但要注意,可中止的代码应该运行得快速不阻塞。这可能会导致整个应用程序域卸载操作挂起。

如果使用与第一个变量(不安全)等效,为什么在finally中检查null?

检查 null 使 using 模式在存在 null 引用时更加安全。


2
语言规范明确说明第一个是正确的。
MS规范(Word文档) ECMA规范
如果线程中止,两种代码变体都可能失败。如果中止发生在表达式被评估之后但在分配给本地变量之前,则第二个变体会失败。
但是您不应该使用线程中止,因为它很容易破坏应用程序域的状态。因此,只有在强制卸载应用程序域时才中止线程。

谢谢,这份文档非常有用。 - Dzinx

2
您关注的问题不正确。ThreadAbortException同样有可能终止OpenText()方法。您可能希望它对此具有弹性,但实际上并没有。框架方法没有try/catch子句来尝试处理线程中止。
请注意,文件不会一直保持打开状态。FileStream终结器最终会关闭文件句柄。当您继续运行并尝试在终结器运行之前再次打开文件时,这当然仍然可能导致程序异常。尽管如此,当您在多任务操作系统上运行时,始终需要采取防御措施。

0

前者确实与后者完全等同。

正如已经指出的那样,ThreadAbort确实是一件糟糕的事情,但它不完全等同于使用任务管理器杀死任务或关闭电脑。

ThreadAbort是一种受控异常,只有在可能的情况下运行时才会引发。

话虽如此,一旦进入ThreadAbort,何必费力清理呢?你已经处于垂死挣扎中了。


-2

finally语句总是会被执行,MSDN上说“finally 用于保证一个代码块在try块如何退出时都能被执行”。

所以您不必担心不清理资源等问题(只有当发生诸如Windows、框架运行时或其他无法控制的错误时,才会出现比清理资源更大的问题;-))


1
那有什么帮助呢?如果在“reader”赋值之前(但文件已打开)发生了中止,它将为null,finally语句不会执行任何操作。 - adrianm
2
你没有注意到其中的细节。文档说明finally无论如何都会运行,这假设它确实被退出了。在某些情况下,控制流程永远不会离开try块,因此finally块在这些情况下永远不会运行。例如,如果保护区域快速失败。 - Eric Lippert
@Eric 在我看来(并且一些快速测试也证实了它),只要try块中的一行代码被执行,你就可以确定无论发生什么,finally块都会被执行。 - Tokk
5
@Tokk:我告诉你,这是个交易:在你给我一百万美元之后,我会永远给你一艘船。你有一百万美元。这是否合乎逻辑地意味着你一定会在生命中的某个时刻得到一艘船?当然不是。你可能会活得很长时间,却从未给过我那一百万美元。或者你可能死了,手里仍拿着那一百万美元。无论哪种情况,你都不会得到一艘船。finally块只有在控制离开try时才会运行。如果控制从未离开try,或者在控制停留在try的时候进程被完全销毁,那么finally如何运行? - Eric Lippert
2
@Eric 我以为我去世后你会偷走我的钱,最终买下你的小船 :-D - Tokk
显示剩余3条评论

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