I've just finished reading "C# 4.0 in a Nutshell" (O'Reilly) and I think it's a great book for a programmer willing to switch to C#, but it left me wondering. My problem is the definition of using
statement. According to the book (p. 138),
using (StreamReader reader = File.OpenText("file.txt")) {
...
}
is precisely equivalent to:
StreamReader reader = File.OpenText("file.txt");
try {
...
} finally {
if (reader != null)
((IDisposable)reader).Dispose();
}
Suppose, however, that this is true and that this code is executed in a separate thread. This thread is now aborted with thread.Abort()
, so a ThreadAbortException
is thrown and suppose the thread is exactly after initializing the reader and before entering the try..finally
clause. This would mean that the reader is not disposed!
A possible solution would be to code t开发者_开发百科his way:
StreamReader reader = null;
try {
reader = File.OpenText("file.txt");
...
} finally {
if (reader != null)
((IDisposable)reader).Dispose();
}
This would be abort-safe.
Now for my questions:
- Are authors of the book right and the
using
statement is not abort-safe or are they wrong and it behaves like in my second solution? - If
using
is equivalent to the first variant (not abort-safe), why does it check fornull
infinally
? - According to the book (p. 856),
ThreadAbortException
can be thrown anywhere in managed code. But maybe there are exceptions and the first variant is abort-safe after all?
EDIT: I know that using thread.Abort()
is not considered good practice. My interest is purely theoretical: how does the using
statement behave exactly?
The book's companion web site has more info on aborting threads here.
In short, the first translation is correct (you can tell by looking at the IL).
The answer to your second question is that there may be scenarios where the variable can be legitimately null. For instance, GetFoo() may return null here, in which you wouldn't want a NullReferenceException thrown in the implicit finally block:
using (var x = GetFoo())
{
...
}
To answer your third question, the only way to make Abort safe (if you're calling Framework code) is to tear down the AppDomain afterward. This is actually a practical solution in many cases (it's exactly what LINQPad does whenever you cancel a running query).
There's really no difference between your two scenarios -- in the second, the ThreadAbort could still happen after the call to OpenText, but before the result is assigned to the reader.
Basically, all bets are off when you get a ThreadAbortException. That's why you should never purposely abort threads rather than using some other method of gracefully bringing the thread to a close.
In response to your edit -- I would point out again that your two scenarios are actually identical. The 'reader' variable will be null unless the File.OpenText call successfully completes and returns a value, so there's no difference between writing the code out the first way vs. the second.
Thread.Abort
is very very bad juju; if people are calling that you're already in a lot of trouble (unrecoverable locks, etc). Thread.Abort
should really be limited to the scanerio of inhuming a sickly process.
Exceptions are generally unrolled cleanly, but in extreme cases there is no guarantee that every bit of code can execute. A more pressing example is "what happens if the power fails?".
Re the null
check; what if File.OpenText
returned null
? OK, it won't but the compiler doesn't know that.
A bit offtopic but the behaviour of the lock statement during thread abortion is interesting too. While lock is equivalent to:
object obj = x;
System.Threading.Monitor.Enter(obj);
try {
…
}
finally {
System.Threading.Monitor.Exit(obj);
}
It is guaranteed(by the x86 JITter) that the thread abort doesn't occur between Monitor.Enter and the try statement.
http://blogs.msdn.com/b/ericlippert/archive/2007/08/17/subtleties-of-c-il-codegen.aspx
The generated IL code seems to be different in .net 4:
http://blogs.msdn.com/b/ericlippert/archive/2009/03/06/locks-and-exceptions-do-not-mix.aspx
The language spec clearly states that the first one is correct.
http://msdn.microsoft.com/en-us/vcsharp/aa336809.aspx MS Spec(Word document)
http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-334.pdf ECMA Spec
In case of a thread aborting both code variants can fail. The second one if the abort occurs after the expression has been evaluated but before the assignment to the local variable occurred.
But you shouldn't use thread abortion anyways since it can easily corrupt the state of the appdomain. So only abort threads if you force unload an appdomain.
You are focusing on the wrong problem. The ThreadAbortException is just as likely to abort the OpenText() method. You might hope that it is resilient to that but it isn't. The framework methods do not have try/catch clauses that try to deal with a thread abort.
Do note that the file doesn't remain opened forever. The FileStream finalizer will, eventually, close the file handle. This of course can still cause exceptions in your program when you keep running and try to open the file again before the finalizer runs. Albeit that this is something you always have to be defensive about when you run on a multi-tasking operating system.
Are authors of the book right and the using statement is not abort-safe or are they wrong and it behaves like in my second solution?
According to the book (p. 856), ThreadAbortException can be thrown anywhere in managed code. But maybe there are exceptions and the first variant is abort-safe after all?
The authors are right. The using
block is not abort-safe. Your second solution is also not abort-safe, the thread could be aborted in the middle of the resource acquisition.
Although it's not abort-safe, any disposable that has unmanged resources should also implement a finalizer, which will eventually run and clean up the resource. The finalizer should be robust enough to also take care of not completely initialized objects, in case the thread aborts in the middle of the resource acquisition.
A Thread.Abort
will only wait for code running inside Constrained Execution Regions (CERs), finally
blocks, catch
blocks, static constructors, and unmanaged code. So this is an abort-safe solution (only regarding the acquisition and disposal of the resource):
StreamReader reader = null;
try {
try { }
finally { reader = File.OpenText("file.txt"); }
// ...
}
finally {
if (reader != null) reader.Dispose();
}
But be careful, abort-safe code should run fast and not block. It could hang a whole app domain unload operation.
If using is equivalent to the first variant (not abort-safe), why does it check for null in finally?
Checking for null makes the using
pattern safe in the presence of null
references.
The former is indeed exactly equivalent to the latter.
As already pointed out, ThreadAbort is indeed a bad thing, but it's not quite the same as killing the task with Task Manager or switching off your PC.
ThreadAbort is an managed exception, which the runtime will raise when it is possible, and only then.
That said, once you're into ThreadAbort, why bother trying to cleanup? You're in death throes anyway.
the finally-statement is always executed, MSDN says "finally is used to guarantee a statement block of code executes regardless of how the preceding try block is exited."
So you don't have to worry about not cleaning resources etc (only if windows, the Framework-Runtime or anything else bad you can't control happens, but then there are bigger problems than cleaning up Resources ;-))
精彩评论