Okay, I finally have a real answer. I sort of worked it out on my own, but only after Lucian Wischik from the VB part of the team confirmed that there really is a good reason for it. Many thanks to him - and please visit his blog (on archive.org), which rocks.
The value 0 here is only special because it's not a valid state which you might be in just before the await
in a normal case. In particular, it's not a state which the state machine may end up testing for elsewhere. I believe that using any non-positive value would work just as well: -1 isn't used for this as it's logically incorrect, as -1 normally means "finished". I could argue that we're giving an extra meaning to state 0 at the moment, but ultimately it doesn't really matter. The point of this question was finding out why the state is being set at all.
The value is relevant if the await ends in an exception which is caught. We can end up coming back to the same await statement again, but we mustn't be in the state meaning "I'm just about to come back from that await" as otherwise all kinds of code would be skipped. It's simplest to show this with an example. Note that I'm now using the second CTP, so the generated code is slightly different to that in the question.
Here's the async method:
static async Task<int> FooAsync()
{
var t = new SimpleAwaitable();
for (int i = 0; i < 3; i++)
{
try
{
Console.WriteLine("In Try");
return await t;
}
catch (Exception)
{
Console.WriteLine("Trying again...");
}
}
return 0;
}
Conceptually, the SimpleAwaitable
can be any awaitable - maybe a task, maybe something else. For the purposes of my tests, it always returns false for IsCompleted
, and throws an exception in GetResult
.
Here's the generated code for MoveNext
:
public void MoveNext()
{
int returnValue;
try
{
int num3 = state;
if (num3 == 1)
{
goto Label_ContinuationPoint;
}
if (state == -1)
{
return;
}
t = new SimpleAwaitable();
i = 0;
Label_ContinuationPoint:
while (i < 3)
{
// Label_ContinuationPoint: should be here
try
{
num3 = state;
if (num3 != 1)
{
Console.WriteLine("In Try");
awaiter = t.GetAwaiter();
if (!awaiter.IsCompleted)
{
state = 1;
awaiter.OnCompleted(MoveNextDelegate);
return;
}
}
else
{
state = 0;
}
int result = awaiter.GetResult();
awaiter = null;
returnValue = result;
goto Label_ReturnStatement;
}
catch (Exception)
{
Console.WriteLine("Trying again...");
}
i++;
}
returnValue = 0;
}
catch (Exception exception)
{
state = -1;
Builder.SetException(exception);
return;
}
Label_ReturnStatement:
state = -1;
Builder.SetResult(returnValue);
}
I had to move Label_ContinuationPoint
to make it valid code - otherwise it's not in the scope of the goto
statement - but that doesn't affect the answer.
Think about what happens when GetResult
throws its exception. We'll go through the catch block, increment i
, and then loop round again (assuming i
is still less than 3). We're still in whatever state we were before the GetResult
call... but when we get inside the try
block we must print "In Try" and call GetAwaiter
again... and we'll only do that if state isn't 1. Without the state = 0
assignment, it will use the existing awaiter and skip the Console.WriteLine
call.
It's a fairly tortuous bit of code to work through, but that just goes to show the kinds of thing that the team has to think about. I'm glad I'm not responsible for implementing this :)
The lock
statement is translated by C# 3.0 to the following:
var temp = obj;
Monitor.Enter(temp);
try
{
// body
}
finally
{
Monitor.Exit(temp);
}
In C# 4.0 this has changed and it is now generated as follows:
bool lockWasTaken = false;
var temp = obj;
try
{
Monitor.Enter(temp, ref lockWasTaken);
// body
}
finally
{
if (lockWasTaken)
{
Monitor.Exit(temp);
}
}
You can find more info about what Monitor.Enter
does here. To quote MSDN:
Use Enter
to acquire the Monitor on
the object passed as the parameter. If
another thread has executed an Enter
on the object but has not yet executed
the corresponding Exit
, the current
thread will block until the other
thread releases the object. It is
legal for the same thread to invoke
Enter
more than once without it
blocking; however, an equal number of
Exit
calls must be invoked before
other threads waiting on the object
will unblock.
The Monitor.Enter
method will wait infinitely; it will not time out.
Best Solution
No, it is not at all difficult or impossible to implement -- the fact that you implemented it yourself is a testament to that fact. Rather, it is an incredibly bad idea and so we don't allow it, so as to protect you from making this mistake.
Correct, you have discovered why we made it illegal. Awaiting inside a lock is a recipe for producing deadlocks.
I'm sure you can see why: arbitrary code runs between the time the await returns control to the caller and the method resumes. That arbitrary code could be taking out locks that produce lock ordering inversions, and therefore deadlocks.
Worse, the code could resume on another thread (in advanced scenarios; normally you pick up again on the thread that did the await, but not necessarily) in which case the unlock would be unlocking a lock on a different thread than the thread that took out the lock. Is that a good idea? No.
I note that it is also a "worst practice" to do a
yield return
inside alock
, for the same reason. It is legal to do so, but I wish we had made it illegal. We're not going to make the same mistake for "await".