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 this 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?
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.
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:
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).
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 ;-))
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):But be careful, abort-safe code should run fast and not block. It could hang a whole app domain unload operation.
Checking for null makes the
using
pattern safe in the presence ofnull
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.
A bit offtopic but the behaviour of the lock statement during thread abortion is interesting too. While lock is equivalent to:
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