异步CTP和“终于”

时间:2011-02-17 18:07:48

标签: c# .net c#-4.0 async-ctp

以下是代码:

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" );
    }
}

这是输出:

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

正如您所看到的,finally块在很多后执行,然后您会期望。

有任何解决方法吗?

提前致谢!

2 个答案:

答案 0 :(得分:13)

这是一个很好的捕获 - 我同意这里的CTP实际上存在一个错误。我挖了它,这就是发生了什么:

这是异步编译器转换的CTP实现的组合,以及.NET 4.0+的TPL(任务并行库)的现有行为。以下是正在发挥作用的因素:

  1. 来自源的最终身体被翻译成真正的CLR最终身体的一部分。出于多种原因,这是可取的,其中一个原因是我们可以让CLR执行它而不会在额外的时间内捕获/重新抛出异常。这也在某种程度上简化了我们的代码生成 - 更简单的代码生成一旦编译就会产生更小的二进制文件,这是我们许多客户所希望的。 :)
  2. Task方法的首要Func(int n)是一个真正的TPL任务。当您await中的Consumer()时,Consumer()方法的其余部分实际上是作为从Task返回的Func(int n)完成后的继续安装的。
  3. CTP编译器转换异步方法的方式导致return在实际返回之前映射到SetResult(...)调用。 SetResult(...)归结为对TaskCompletionSource<>.TrySetResult的调用。
  4. TaskCompletionSource<>.TrySetResult表示完成了TPL任务。立即使其延续“有时”发生。这个“某个时间”可能意味着在另一个线程上,或者在某些情况下TPL是聪明的并且说“嗯,我不妨现在在同一个线程上调用它”。
  5. Task的首要Func(int n)在最终运行之前就在技术上变为“已完成”。这意味着等待异步方法的代码可能在并行线程中运行,甚至在finally块之前运行。
  6. 考虑到总体Task应该代表该方法的异步状态,从根本上说它不应该被标记为已完成,直到至少所有用户提供的代码都按照语言设计执行。我将与Anders,语言设计团队和编译器开发人员讨论这个问题。


    表现/严重程度的范围:

    在WPF或WinForms情况下,您通常不会对此有所了解,因为您正在进行某种托管消息循环。原因是await实施Task遵循SynchronizationContext。这会导致异步连续在预先存在的消息循环上排队,以便在同一个线程上运行。您可以通过以下方式更改代码以运行Consumer()来验证这一点:

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

    在WPF消息循环的上下文中运行后,输出将按预期显示:

    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
    

    解决方法:

    唉,解决方法意味着将代码更改为不在return块中使用try/finally语句。我知道这真的意味着你在代码流中失去了很多优雅。您可以使用异步辅助方法或帮助程序lambdas来解决此问题。就个人而言,我更喜欢helper-lambdas,因为它会自动关闭包含方法的locals /参数,并使相关代码保持更近。

    Helper Lambda方法:

    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;
    }
    

    帮助方法方法:

    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;
    }
    

    作为一个无耻的插件:我们正在微软的语言空间招聘,并一直在寻找优秀的人才。博客条目here以及未结头寸的完整列表:)

答案 1 :(得分:2)

修改

请考虑Theo Yaung的answer

原始答案

我不熟悉async / await,但在阅读之后: Visual Studio Async CTP Overview

并阅读您的代码,我在await函数中看到Func(int n),这意味着从{i>之后的<{i> await关键字直到结束时函数稍后将作为代理执行。

所以我的猜测(这是一个没有受过教育的猜测)是Func:BeginFunc:End可能会在不同的“上下文”(线程?)中执行,即,异步。

因此,int u = await Func( i );中的Consumer行将在await中的代码Func到达时继续执行。所以很有可能:

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

Func: EndFunc: Finally可以显示在日志中的任何位置,唯一的限制是Func: End #X将出现在其关联的Func: Finally #X之前,并且两者都应该出现在After the wait之前。

正如Henk Holterman所解释的那样(有点突然),你在await体内放置Func的事实意味着之后的所有事情都会在之后执行。< / p>

没有解决方法,by design您在awaitBegin End之间放置Func

只是我未受过教育的2欧元。