Here's the code:
static class AsyncFinally
{
static async Task<int> Func( int n )
{
try
{
Console.WriteLine( " Func: Begin #{0}", n );
await TaskEx.Delay( 100 );
Console.WriteLine( " Func: End #{0}", n );
return 0;
}
finally
{
Console.WriteLine( " Func: Finally #{0}", n );
}
}
static async Task Consumer()
{
for ( int i = 1; i <= 2; i++ )
{
Console.WriteLine( "Consumer: before await #{0}", i );
int u = await Func( i );
Console.WriteLine( "Consumer: after await #{0}", i );
}
Console.WriteLine( "Consumer: after the loop" );
}
public static void AsyncTest()
{
Task t = TaskEx.RunEx( Consumer );
t.Wait();
Console.WriteLine( "After the wait" );
}
}
Here's the output:
Consumer: before await #1
Func: Begin #1
Func: End #1
Consumer: after await #1
Consumer: before await #2
Func: Begin #2
Func: Finally #1
Func: End #2
Consumer: after await #2
Consumer: after the loop
Func: Finally #2
After the wait
As you can see, the finally block is executed much later then you'd expect.
Any workarounds?
Thanks in advance!
This is an excellent catch - and I agree that there is actually a bug in the CTP here. I dug into it and here's what's going on:
This is a combination of the CTP implementation of the async compiler transformations, as well as the existing behavior of the TPL (Task Parallel Library) from .NET 4.0+. Here are the factors at play:
Task
for theFunc(int n)
method is a real TPL task. When youawait
inConsumer()
, then the rest of theConsumer()
method is actually installed as a continuation off of the completion of theTask
returned fromFunc(int n)
.return
being mapped to aSetResult(...)
call prior to a real return.SetResult(...)
boils down to a call toTaskCompletionSource<>.TrySetResult
.TaskCompletionSource<>.TrySetResult
signals the completion of the TPL task. Instantly enabling its continuations to occur "sometime". This "sometime" may mean on another thread, or in some conditions the TPL is smart and says "um, I might as well just call it now on this same thread".Task
forFunc(int n)
becomes technically "Completed" right before the finally gets run. This means that code that was awaiting on an async method may run in parallel threads, or even before the finally block.Considering the overarching
Task
is supposed to represent the asynchronous state of the method, fundamentally it shouldn't get flagged as completed until at least all the user-provided code has been executed as per the language design. I'll bring this up with Anders, language design team, and compiler devs to get this looked at.Scope of Manifestation / Severity:
You typically won't be bit by this as bad in a WPF or WinForms case where you have some sort of managed message loop going on. The reason why is that the
await
onTask
implementations defer to theSynchronizationContext
. This causes the async continuations to be queued up on the pre-existing message loop to be run on the same thread. You can verify this by changing your code to runConsumer()
in the following way:Once run inside the context of the WPF message loop, the output appears as you would expect it:
Workaround:
Alas, the workaround means changing your code to not use
return
statements inside atry/finally
block. I know this really means you lose a lot of elegance in your code flow. You can use async helper methods or helper lambdas to work around this. Personally, I prefer the helper-lambdas because it automatically closes over locals/parameters from the containing method, as well as keeps your relevant code closer.Helper Lambda approach:
Helper Method approach:
As a shameless plug: We're hiring in the languages space at Microsoft, and always looking for great talent. Blog entry here with the full list of open positions :)