Why would try/finally rather than a “using” statement help avoid a race condition?

后端 未结 4 1710
眼角桃花
眼角桃花 2020-11-29 07:29

This question relates to a comment in another posting here: Cancelling an Entity Framework Query

I will reproduce the code example from there for clarity:

         


        
相关标签:
4条回答
  • 2020-11-29 07:52

    Very strange, cause using is only syntax sugar for try - finally block.

    From MSDN:

    You can achieve the same result by putting the object inside a try block and then calling Dispose in a finally block; in fact, this is how the using statement is translated by the compiler.

    0 讨论(0)
  • 2020-11-29 08:03

    That comment doesn't make any sense as a using statement will be translated to a try/finally block by the compiler. Since 'entities' will not be used outside of the scope, it's easier to use a using statment as this will automatically dispose of the resources.

    You can read more about this on MSDN: using Statement (C# Reference).

    0 讨论(0)
  • 2020-11-29 08:05

    Depending on whether you use using or explicitly try/finally you can have slightly different code using the sample code you will have

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

    substituting that with a using statement it could look like

       using(var entities = new AdventureWorks2008R2Entities()){
          ...
       }
    

    which accordig do §8.13 of the specification will be expanded to

        AdventureWorks2008R2Entities entities = new AdventureWorks2008R2Entities();
        try
        {
            ...
        } finally {
        } 
    

    Therefor the only real difference is that the assignment is not within the try/finally block but that has no consequence on which race conditions could occur (aside from thread abort inbetween the assignement and the try block as Hans also notes)

    0 讨论(0)
  • 2020-11-29 08:12

    Yeah, there is a possible race in the using statement. The C# compiler transforms

    using (var obj = new Foo()) {
        // statements
    }
    

    to:

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

    The race occurs when the thread is aborted right between the obj assignment statement and the try block. Extremely small odds but not zero. The object won't be disposed when that happens. Note how he rewrote that code by moving the assignment inside the try block so this race cannot occur. Nothing actually goes fundamentally wrong when the race occurs, disposing objects is not a requirement.

    Having to choose between making thread aborts marginally more efficient and writing using statements by hand, you should first opt for not getting in the habit of using Thread.Abort(). I can't recommend actually doing this, the using statement has additional safety measures to ensure accidents don't happen, it makes sure that the original object gets disposed even when the object is re-assigned inside the using statement. Adding catch clauses is less prone to accidents as well. The using statement exists to reduce the likelihood of bugs, always use it.


    Noodling on a bit about this problem, the answer is popular, there's another common C# statement that suffers from the exact same race. It looks like this:

    lock (obj) {
        // statements
    }
    

    Translated to:

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

    Exact same scenario, the thread abort can strike after the Enter() call and before entering the try block. Which prevents the Exit() call from being made. This is way nastier than a Dispose() call that isn't made of course, this is almost certainly going to cause deadlock. The problem is specific to the x64 jitter, the sordid details are described well in this Joe Duffy blog post.

    It is very hard to reliably fix this one, moving the Enter() call inside the try block can't solve the problem. You cannot be sure that the Enter call was made so you cannot reliably call the Exit() method without possibly triggering an exception. The Monitor.ReliableEnter() method that Duffy was talking about did eventually happen. The .NET 4 version of Monitor got a TryEnter() overload that takes a ref bool lockTaken argument. Now you know it is okay to call Exit().

    Well, scary stuff that goes BUMP in the night when you are not looking. Writing code that's safely interruptable is hard. You'd be wise to never assume that code that you didn't write got all of this taken care of. Testing such code is extremely difficult since the race is so rare. You can never be sure.

    0 讨论(0)
提交回复
热议问题