为什么try/finally比“using”语句更有助于避免竞态条件?

25
这个问题与另一篇帖子中的评论有关:取消 Entity Framework 查询 为了更清晰地说明,我将在此处重现那里的代码示例:
    var thread = new Thread((param) =>
    {
        var currentString = param as string;

        if (currentString == null)
        {
            // TODO OMG exception
            throw new Exception();
        }

        AdventureWorks2008R2Entities entities = null;
        try // Don't use using because it can cause race condition
        {
            entities = new AdventureWorks2008R2Entities();

            ObjectQuery<Person> query = entities.People
                .Include("Password")
                .Include("PersonPhone")
                .Include("EmailAddress")
                .Include("BusinessEntity")
                .Include("BusinessEntityContact");
            // Improves performance of readonly query where
            // objects do not have to be tracked by context
            // Edit: But it doesn't work for this query because of includes
            // query.MergeOption = MergeOption.NoTracking;

            foreach (var record in query 
                .Where(p => p.LastName.StartsWith(currentString)))
            {
                // TODO fill some buffer and invoke UI update
            }
        }
        finally
        {
            if (entities != null)
            {
                entities.Dispose();
            }
        }
    });

thread.Start("P");
// Just for test
Thread.Sleep(500);
thread.Abort();

我无法理解这条评论的意思,它说:

不要使用 using,因为它可能会引起竞态条件

entities 是局部变量,如果在另一个线程上重新进入代码,则不会被共享,在同一线程内,将其赋值到 "using" 语句中似乎非常安全(实际上等同于给定代码),而不是使用 try/finally 手动完成。有人可以给我解释一下吗?

11
你应该询问原作者。对我来说没有意义。 - Jon Skeet
为什么不去问那段代码的创作者呢? - sloth
这是我最初的想法,但不幸的是,我找不到与他联系的方式,他的博客上没有电子邮件,也没有在这里或MSDN论坛上发送个人消息的方法(如果我太愚蠢而错过了,请指点我正确的方向!) - Mike Nunan
4个回答

44

是的,在 using 语句中可能存在竞争条件。 C# 编译器进行转换。

using (var obj = new Foo()) {
    // statements
}
var obj = new Foo();
try {
   // statements
}
finally {
   if (obj != null) obj.Dispose();
}

当线程在obj赋值语句和try块之间被中止时,就会发生竞态条件。虽然概率极小但不为零。当发生这种情况时,对象将不会被处理。请注意,作者通过将赋值语句移动到try块内部重写了代码,以防止出现这种竞态条件。当竞态条件发生时,实际上并没有什么根本错误,释放对象不是必须的。

如果必须在使线程终止略微更有效和手工编写using语句之间进行选择,则应首选不养成使用Thread.Abort()的习惯。我不能建议实际这样做,using语句有额外的安全措施来确保不会发生意外事故,它还可以确保即使在using语句内部重新分配对象时,原始对象也会得到处理。添加catch子句的错误机率也较低。 using语句的存在是为了减少bug的可能性,一定要使用它。


对此问题进行一番思考后,我们发现还有另一个常见的C#语句也会受到完全相同的竞态条件影响。它长这个样子:

lock (obj) {
    // statements
}

翻译为:

Monitor.Enter(obj);
// <=== Eeeek!
try {
    // statements
}
finally {
    Monitor.Exit(obj);
}

完全相同的情况是,在调用Enter()之后以及进入try块之前,线程可能被中止,从而阻止了Exit()方法的调用。这比没有调用Dispose()要糟糕得多,几乎肯定会导致死锁。这个问题特定于x64 jitter,详细信息在Joe Duffy的博客文章中描述得很好。

解决这个问题非常困难,将Enter()调用移到try块内部无法解决问题。您不能确定是否进行了Enter()调用,因此可能触发异常而无法可靠地调用Exit()方法。Duffy所说的Monitor.ReliableEnter()方法最终确实存在。 .NET 4版本的Monitor获得了一个TryEnter()重载,它带有一个ref bool lockTaken参数。现在您知道可以安全地调用Exit()方法了。

写安全可中断代码是一项艰巨的任务。您最好不要假设由其他人编写的代码已经考虑到了所有这些情况。测试这种代码非常困难,因为竞争如此罕见。你永远无法确定。


感谢Hans(也感谢其他所有人的回复)。所以这不是病态情况,但如果要精确地说,它确实符合竞争条件。关于使用Thread.Abort(),我完全同意最好避免使用,但在这种情况下,实际上没有其他选择,因为Entity Framework 4.x不支持中断查询,并且我与其他问题的OP面临相同的问题,因为我正在响应用户操作发出查询,如果用户做其他事情需要尽早中断。 - Mike Nunan
2
我必须遵循使用EF的常识。但是你评论中有一个巨大的红旗。“不支持中断查询”只是说“不支持线程中止”的委婉说法。如果EF实际上支持线程中断,那么添加一种中断查询的方式就不会成为问题。并且已经被添加到API中,这是一个明显的理想功能。你绝对在冒险。 - Hans Passant
是的,他们已经在EF 6.x中添加了它,但那还在测试版阶段,我们使用的Oracle客户端库没有支持,所以我现在只能使用这个hack。当我说“中断查询”时,我不太准确 - 我真正的意思是没有正确的异步/任务式取消,因此,即使结果变得无关紧要,您也必须使用Thread.Abort()来终止查询。 - Mike Nunan
1
当竞争发生时,实际上并没有什么根本性的错误,处理对象并不是必需的。唯一我不同意这个说法的情况是,如果构造函数分配或打开某些外部资源,比如池化数据库连接等。在这种情况下,池中可供其他线程使用的连接将减少一个。如果这样做足够多次,您将无法再获得与数据库的连接。然而,通常情况下,我不会担心这个问题。 - Tony Vitabile

7

很奇怪,因为using只是一种语法糖,用于try-finally块。

来自MSDN:

您可以通过将对象放置在try块内,然后在finally块中调用Dispose来实现相同的结果; 实际上,这就是编译器将using语句转换的方式。


4

根据您使用using还是显式try/finally,您可以使用略有不同的代码,使用示例代码即可。

    AdventureWorks2008R2Entities entities = null;
    try // Don't use using because it can cause race condition
    {
        entities = new AdventureWorks2008R2Entities();
        ...
    } finally {
    } 

使用using语句来替换它,代码如下:
   using(var entities = new AdventureWorks2008R2Entities()){
      ...
   }

根据规范第8.13节的要求,此内容将被扩展为:
    AdventureWorks2008R2Entities entities = new AdventureWorks2008R2Entities();
    try
    {
        ...
    } finally {
    } 

因此,唯一的真正区别在于赋值不在try/finally块内,但这对可能发生的竞态条件没有影响(除了线程在分配和try块之间中止,正如Hans所指出的那样)。

2

这条评论毫无意义,因为using语句将被编译器转换为try/finally代码块。由于'entities'不会在范围之外使用,因此最好使用using语句,这将自动处理资源的释放。

您可以在MSDN上阅读更多相关信息:using语句 (C#参考)


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