Print

Code Consistency With Asynchronous Exceptions

Three weeks ago Joe Duffy published an article about 'Monitor.Enter, thread aborts, and orphaned locks' on his blog. After reading this article and responding to it (see my comments), I was pretty much in shock. I came to the conclusion that the C# ‘using’ statement isn't 100% safe.

After pulling myself together, I read Joe's article from March 2005 about 'Atomicity and asynchronous exception failures'. I should have read this article before commenting in the first place, because that article explains the problem thoroughly (I'm sorry Joe ;-)).

The May 2005 article was initially written as an internal mail to his coworkers, but he decided to share it with the rest of the world. In short the article describes that the framework doesn’t guarantee atomic paired operations (read: try-finally blocks) to be executed. In case of (and only in case of) an asynchronous exception (read: thread abort exceptions) there is a small possibility of inconsistent behavior of code. This only holds for asynchronous exceptions, so this excludes exceptions that are thrown from your code or code you call.

The conclusion however, is that the problem isn't as bad as it seems and normally we shouldn't worry about this, because:

  • problems only occur with asynchronous exceptions (read thread abort exceptions);
  • those asynchronous thread abort exceptions are, however, pretty rare;
  • thread aborts are not processed 'inside a Constrained Execution Region (CER), finally block, catch block, .cctor, or while running inside unmanaged code' [->];
  • finalizers will clean-up orphan resources eventually;
  • most of the time the AppDomain will be unloaded shortly after a thread abort. (Unloading an AppDomain calls all finalizers on objects);
  • the 'cases are so sporadic and difficult to predict that developers shouldn't proactively seek to fix problems that might not exist' [->].

So let's dive into some very dirty details (I love details) to see where this might go wrong. Let's start with a statement we all write on a daily base.

using (Resource r = new Resource())
{
r.DoSomeWork();
}

The code above shows a simple using statement. It creates a new resource, we use it, and it will be disposed by a call to it's dispose method. The C# compiler will convert this into a try-finally statement like shown below:

Resource r = new Resource();
try
{
r.DoSomeWork();
}
finally
{
if (r != null) ((IDisposable)r).Dispose();
}

And this will, in it's turn, be compiled to intermediate language (IL):

 // Resource r = new Resource();
L_0000: newobj instance void Resource::.ctor()
L_0005: stloc.0
// try
// {
// r.DoSomeWork();
L_0006: ldloc.0
L_0007: callvirt instance void Resource::DoSomeWork()
L_000c: leave.s L_0018
// }
// finally
// {
// if (r != null)
L_000e: ldloc.0
L_000f: brfalse.s L_0017
// {
// ((IDisposable)r).Dispose();
L_0011: ldloc.0
L_0012: callvirt instance void
[mscorlib]System.IDisposable::Dispose()
// }
L_0017: endfinally
// }
L_0018: ret
.try L_0006 to L_000e finally handler L_000e to L_0018

And the JIT may compile this down to the following assembly instructions (only the first eight instructions are shown):

00000024  xor   edx,edx 
00000026 mov dword ptr [ebp-28h],edx
00000029 mov ecx,999634h
// Call .ctor on type Resource
0000002e call FFCA0E64
00000033 mov esi,eax
00000035 mov ecx,esi
00000037 call dword ptr ds:[00999670h]
0000003d mov dword ptr [ebp-28h],esi
// try blocks starts here

Now look at the constructor of our Resource Type:

public Resource()
{
this._connection =
new SqlConnection(connString);
// Resource acquired here
this._connection.Open();
}

The JITted assembly for our constructor looks like this:

 // public Resource()
00000000 push edi
00000001 push esi
00000002 mov esi,ecx
00000004 cmp dword ptr ds:[00998860h],0
0000000b je 00000012
0000000d call 793B1076
00000012 mov ecx,esi
00000014 call 78669220
// {
// _connection =
// new SqlConnection(connString);
00000019 mov ecx,653C363Ch
0000001e call 791ACE1C
00000023 mov edi,eax
00000025 mov edx,dword ptr ds:[02305A34h]
0000002b mov ecx,edi
0000002d call 647A5014
00000032 lea edx,[esi+4]
00000035 call 791927C7
// _connection.Open();
0000003a mov ecx,dword ptr [esi+4]
0000003d mov eax,dword ptr [ecx]
0000003f call dword ptr [eax+000000B8h]
// }
00000045 nop
00000046 pop esi
00000047 pop edi
00000048 ret

Now let us analyze this code. Between the acquisition of the resource in the Resource constructor and the start of the try block, there are at least 6 assembly instructions. Which are the following:

 // Last four instruction in the Resource constructor
00000045 nop
00000046 pop esi
00000047 pop edi
00000048 ret

// Instructions right after the the call to the
// constructor method (call FFCA0E64).
00000033 mov esi,eax
00000035 mov ecx,esi

It's even possible that there are more than six instructions, because I think the call to the subroutine that follows the last move (instruction 00000037) initializes the try and there are possibly some relevant instructions at the end of the _connection.Open() subroutine too (but I’ll let those out of the picture).

So we can conclude that there are AT LEAST six assembly instructions between the acquisition of the resource (connection.Open) and the initialization of the try block (call dword ptr ds:[00999670h]). When a thread abort gets raised in this window, then Dispose will never get called on the instance that is created.

It is obvious that the bigger the gap between the acquisition and the beginning of the try-block, the bigger the chance of failure. Besides, as the number of processors in system increase, the higher the chance of failure.

But in fact the number of assembly instructions isn't really the problem here, because even if there were no assembly instructions the problem would still existed. To quote Joe:

speaking in terms of absolutes: it is ALWAYS possible that your cleanup code won't execute. [...] A fallback plan is always required. Your programs should be able to cope with state corruption that persists due to app crashes, rude shutdowns, and so on. This can be difficult in practice, but if you design it into your code to begin with, it's usually possible... even if it means asking the user to reboot the machine or manually repair some corrupted data (though clearly these are last resorts). [->]

Should I worry about this? I’m now convinced I shouldn’t. All I do anyway comes down to making calls from web apps to SQL Server databases. The chance of a leaking connection by thread aborts is insignificant. Besides, the connection's finalizer will eventually bring the connection back to the pool. Next to that, as Joe points out: 'prefer acquisition of resources inside discrete methods'. This way the resource is acquired within the try block. So the example Resource type shown above is actually a bad practice ;-). I think that there are bigger concerns than leaking connections. Ensuring atomicity through database transactions for instance. But transactions which aren't opened or haven't been written to don't pose a risk. Besides that, the database server ensures atomicity by its design. So in case of a rude shutdown or power failure the database will eventually save my day.

So I can conclude that this uncertainty isn't a problem at all for me, but it still is bloody interesting!

- .NET General, C# - two comments / No trackbacks - §

The code samples on my weblog are colorized using javascript, but you disabled javascript (for my website) on your browser. If you're interested in viewing the posted code snippets in color, please enable javascript.

two comments:

Thanks a bunch for this investigation. Very interesting.

I'm curious (hypothetically speaking, of course) if this could be avoided by the C# language designers or C# compiler folks if the using statement was translated into this instead:

Resource r;
try
{
r = new Resource();
r.DoSomeWork();
}
finally
{
if (r != null) ((IDisposable)r).Dispose();
}
Matt (URL) - 24 09 08 - 00:14

It's Unfortunate, but this doesn't solve the problem. There is still a timeframe between the moment the constructor ran and the time the reference is assigned to the variable 'r'. If you read the comments on Joe's blog, you can see that I asked this same question.
Steven (URL) - 24 09 08 - 09:11


No trackbacks:

Trackback link:

Please enable javascript to generate a trackback url


  
Remember personal info?

/

Before sending a comment, you have to answer correctly a simple question everyone knows the answer to. This completely baffles automated spam bots.
 

  (Register your username / Log in)

Notify:
Hide email:

Small print: All html tags except <b> and <i> will be removed from your comment. You can make links by just typing the url or mail-address.