异步CTP和"终于"

Soo*_*nts 18 .net c# 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" );
    }
}
Run Code Online (Sandbox Code Playgroud)

这是输出:

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
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,finally块会在您预期的时间之后执行.

任何解决方法?

提前致谢!

The*_*ung 13

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

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

  1. 来自源的最终主体被翻译成真正的CLR最终主体的一部分.出于多种原因,这是可取的,其中一个原因是我们可以让CLR执行它而不会在额外的时间内捕获/重新抛出异常.这也在某种程度上简化了我们的代码生成 - 更简单的代码生成一旦编译就会产生更小的二进制文件,这是我们许多客户所希望的.:)
  2. 首要TaskFunc(int n)方法是一个真正的TPL任务.当你await进入时Consumer(),那么该Consumer()方法的其余部分实际上是作为一个延续完成后Task返回的Func(int n).
  3. CTP编译器转换异步方法的方式导致在实际返回之前return映射到SetResult(...)调用.SetResult(...)归结为打电话给TaskCompletionSource<>.TrySetResult.
  4. TaskCompletionSource<>.TrySetResult表示完成TPL任务.立即使其延续"有时"发生.这个"某个时间"可能意味着在另一个线程上,或者在某些情况下TPL是聪明的并且说"嗯,我不妨现在在同一个线程上调用它".
  5. 在最终运行之前,总体而言TaskFunc(int n)技术上变为"已完成".这意味着等待异步方法的代码可以在并行线程中运行,甚至可以在finally块之前运行.

考虑到总体Task应该代表方法的异步状态,从根本上说它不应该被标记为已完成,直到至少所有用户提供的代码都按照语言设计执行.我将与Anders,语言设计团队和编译器开发人员一起讨论这个问题.


表现范围/严重程度:

在WPF或WinForms中你通常会遇到某种托管消息循环,你通常不会觉得这很糟糕.原因是awaiton Task实现遵循SynchronizationContext.这会导致异步连续在预先存在的消息循环上排队,以便在同一个线程上运行.您可以通过更改代码以Consumer()通过以下方式运行来验证这一点:

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

一旦在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
Run Code Online (Sandbox Code Playgroud)

解决方法:

唉,解决方法意味着将代码更改为不使用块return内的语句try/finally.我知道这真的意味着你在代码流中失去了很多优雅.您可以使用异步辅助方法或帮助程序lambda来解决此问题.就个人而言,我更喜欢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;
}
Run Code Online (Sandbox Code Playgroud)

辅助方法方法:

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;
}
Run Code Online (Sandbox Code Playgroud)

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