开发者

Async CTP and "finally"

开发者 https://www.devze.com 2023-02-11 12:23 出处:网络
Here\'s the code: static class AsyncFinally { static async Task<int> Func( int n ) { try { Console.WriteLine( \"Func: Begin #{0}\", n );

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:

  1. The finally body from source is translated into part of a real CLR-finally body. This is desirable for many reasons, one of which is that we can get the CLR to execute it without catching/rethrowing the exception an extra time. This also simplifies our code gen to some degree - simpler code gen results in smaller binaries once compiled, which is definitely desired by many of our customers. :)
  2. The overarching Task for the Func(int n) method is a real TPL task. When you await in Consumer(), then the rest of the Consumer() method is actually installed as a continuation off of the completion of the Task returned from Func(int n).
  3. The way the CTP compiler transforms async methods results in a return being mapped to a SetResult(...) call prior to a real return. SetResult(...) boils down to a call to TaskCompletionSource<>.TrySetResult.
  4. 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".
  5. The overarching Task for Func(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 on Task implementations defer to the SynchronizationContext. 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 run Consumer() in the following way:

    DispatcherFrame frame = new DispatcherFrame(exitWhenRequested: true);
    Action asyncAction = async () => {
        await Consumer();
        frame.Continue = false;
    };
    Dispatcher.CurrentDispatcher.BeginInvoke(asyncAction);
    Dispatcher.PushFrame(frame);

Once run inside the context of the WPF message loop, the output appears as you would expect it:

Consumer: before await #1
    Func: Begin #1
    Func: End #1
    Func: Finally #1
Consumer: after await #1
Consumer: before await #2
    Func: Begin #2
    Func: End #2
    Func: Finally #2
Consumer: after await #2
Consumer: after the loop
After the wait

Workaround:

Alas, the workaround means changing your code to not use return statements inside a try/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:

static async Task<int> Func( int n )
{
    int result;
    try
    {
        Func<Task<int>> helperLambda = async() => {
            Console.WriteLine( "    Func: Begin #{0}", n );
            await TaskEx.Delay( 100 );
            Console.WriteLine( "    Func: End #{0}", n );        
            return 0;
        };
        result = await helperLambda();
    }
    finally
    {
        Console.WriteLine( "    Func: Finally #{0}", n );
    }
    // since Func(...)'s return statement is outside the try/finally,
    // the finally body is certain to execute first, even in face of this bug.
    return result;
}

Helper Method approach:

static async Task<int> Func(int n)
{
    int result;
    try
    {
        result = await HelperMethod(n);
    }
    finally
    {
        Console.WriteLine("    Func: Finally #{0}", n);
    }
    // since Func(...)'s return statement is outside the try/finally,
    // the finally body is certain to execute first, even in face of this bug.
    return result;
}

static async Task<int> HelperMethod(int n)
{
    Console.WriteLine("    Func: Begin #{0}", n);
    await TaskEx.Delay(100);
    Console.WriteLine("    Func: End #{0}", n);
    return 0;
}

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 :)


Edit

Please consider Theo Yaung's answer.

Original answer

I'm not familiar with async/await, but after reading this: Visual Studio Async CTP Overview

and reading your code, I see the await in the Func(int n) function, meaning that from the code after the await keyword till the end of the function will be executed later as a delegate.

So my guess (and this is an uneducated guess) is that the Func:Begin and Func:End will possibly execute in different "contexts" (threads?), i.e., asynchronously.

Thus, the int u = await Func( i ); line in Consumer will continue its execution the moment the code await in Func will be reached. So it is quite possible to have:

Consumer: before await #1
    Func: Begin #1
Consumer: after await #1
Consumer: before await #2
    Func: Begin #2
Consumer: after await #2
Consumer: after the loop
    Func: End #1         // Can appear at any moment AFTER "after await #1"
                         //    but before "After the wait"
    Func: Finally #1     // will be AFTER "End #1" but before "After the wait"
    Func: End #2         // Can appear at any moment AFTER "after await #2"
                         //    but before "After the wait"
    Func: Finally #2     // will be AFTER "End #2" but before "After the wait"
After the wait           // will appear AFTER the end of all the Tasks

The Func: End and Func: Finally can appear in whatever position in the logs, the only constraint being that a Func: End #X will appear before its associated Func: Finally #X, and that both should appear before the After the wait.

As explained (somewhat abruptly) by Henk Holterman is that the fact you put an await in the Func body means that everything after will be executed sometimes after.

There is no workaround as, by design you put a await between the Begin and the End of Func.

Just my uneducated 2 eurocents.

0

精彩评论

暂无评论...
验证码 换一张
取 消