29

我刚刚读完“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抛出 a 并假设线程恰好在初始化读取器之后和进入try..finally子句之前。这意味着读者没有被处置!

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

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

这将是中止安全的。

现在我的问题:

  1. 这本书的作者是对的,并且该using声明不是安全中止的,还是他们错了,它的行为就像我的第二个解决方案一样?
  2. 如果using等效于第一个变体(不是中止安全的),为什么要检查nullin finally
  3. 根据本书 (p. 856),ThreadAbortException可以在托管代码中的任何地方抛出。但也许有例外,第一个变体毕竟是中止安全的?

编辑:我知道使用thread.Abort()不被认为是好的做法。我的兴趣纯粹是理论上的:该using语句的行为如何

4

9 回答 9

18

本书的配套网站在此处提供了有关中止线程的更多信息。

简而言之,第一个翻译是正确的(您可以通过查看 IL 来判断)。

第二个问题的答案是,可能存在变量可以合法为空的情况。例如,GetFoo() 可能在此处返回 null,您不希望在隐式 finally 块中抛出 NullReferenceException:

using (var x = GetFoo())
{
   ...
}

要回答您的第三个问题,使 Abort 安全(如果您正在调用框架代码)的唯一方法是事后拆除 AppDomain。在许多情况下,这实际上是一个实用的解决方案(这正是 LINQPad 在您取消正在运行的查询时所做的)。

于 2010-10-13T13:57:57.730 回答
9

您的两种情况之间确实没有区别 - 在第二种情况下,ThreadAbort 仍然可能在调用 OpenText 之后发生,但在结果分配给读者之前。

基本上,当你得到一个 ThreadAbortException 时,所有的赌注都被取消了。这就是为什么你永远不应该故意中止线程,而不是使用其他一些优雅地关闭线程的方法。

作为对您的编辑的回应——我要再次指出,您的两个场景实际上是相同的。除非 File.OpenText 调用成功完成并返回一个值,否则“reader”变量将为 null ,因此第一种方式与第二种方式编写代码之间没有区别。

于 2010-10-13T12:18:28.253 回答
7

Thread.Abort是非常非常糟糕的juju;如果人们打电话说你已经遇到了很多麻烦(不可恢复的锁等)。Thread.Abort真的应该仅限于扫描一个病态的过程。

异常通常被干净地展开,但在极端情况下,不能保证每一位代码都可以执行。一个更紧迫的例子是“如果电源出现故障会发生什么?”。

重新null检查;如果File.OpenText退回null怎么办?好的,它不会,但编译器不知道。

于 2010-10-13T12:17:26.747 回答
4

有点离题,但线程中止期间锁语句的行为也很有趣。而锁相当于:

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

于 2010-10-13T12:38:37.237 回答
2

语言规范明确指出第一个是正确的。

http://msdn.microsoft.com/en-us/vcsharp/aa336809.aspx微软规范(Word 文档)
http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-334.pdf ECMA 规范

如果线程中止,两种代码变体都可能失败。第二个如果中止发生在对表达式求值之后但在分配给局部变量之前发生。

但是无论如何你都不应该使用线程中止,因为它很容易破坏 appdomain 的状态。因此,仅当您强制卸载应用程序域时才中止线程。

于 2010-10-13T12:27:08.290 回答
2

你专注于错误的问题。ThreadAbortException 很可能会中止 OpenText() 方法。您可能希望它对此具有弹性,但事实并非如此。框架方法没有尝试处理线程中止的 try/catch 子句。

请注意,该文件不会永远保持打开状态。FileStream 终结器最终将关闭文件句柄。当然,当您继续运行并尝试在终结器运行之前再次打开文件时,这仍然会导致程序异常。尽管这是您在多任务操作系统上运行时始终必须防范的事情。

于 2010-10-13T12:44:52.727 回答
2

这本书的作者是对的吗? using 语句不是安全中止的,还是他们错了,它的行为就像我的第二个解决方案一样?

根据这本书 (p. 856),可以在托管代码的任何地方抛出 ThreadAbortException。但也许有例外,第一个变体毕竟是中止安全的?

作者是对的。该using块不是中止安全的。您的第二个解决方案也不是中止安全的,线程可能在资源获取过程中中止。

虽然它不是中止安全的,但任何具有未管理资源的一次性也应该实现一个终结器,最终将运行并清理资源。终结器应该足够健壮以处理未完全初始化的对象,以防线程在资源获取过程中中止。

AThread.Abort只会等待在受限执行区域 (CER)、finally块、catch块、静态构造函数和非托管代码内运行的代码。所以这是一个中止安全的解决方案(关于资源的获取和处置):

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

但要小心,中止安全代码应该运行得快不是阻塞。它可能会挂起整个应用程序域卸载操作。

如果 using 等效于第一个变体(不是中止安全的),为什么它最终检查 null ?

检查 null 使using模式在存在null引用时是安全的。

于 2011-03-02T18:07:57.213 回答
0

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

正如已经指出的那样,ThreadAbort 确实是一件坏事,但它与使用任务管理器终止任务或关闭 PC 并不完全相同。

ThreadAbort 是一个托管异常,运行时将在可能的情况下引发,并且仅在那时引发。

也就是说,一旦你进入 ThreadAbort,为什么还要费心去清理呢?无论如何,你正处于垂死挣扎中。

于 2010-10-13T12:39:47.297 回答
-2

finally 语句总是被执行,MSDN说“finally 用于保证语句块的执行,而不管前面的 try 块是如何退出的。”

所以你不必担心不清理资源等(只有当窗口、框架运行时或其他你无法控制的坏事发生时,但还有比清理资源更大的问题;-))

于 2010-10-13T13:26:07.703 回答