异步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块会比你期望的要晚得多。

任何解决方法?

提前致谢!


这是一个很好的结果 - 我同意CTP中实际存在一个错误。 我深入了解了以下是发生了什么事情:

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

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


    表现/严重性的范围:

    在WPF或WinForms的情况下,你有一些托管的消息循环正在进行,你通常不会因此而受到影响。 之所以这样,是因为Task实现上的await依赖于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
    

    解决方法:

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

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

    作为一个无耻的插件:我们正在招聘微软的语言空间,并且一直在寻找伟大的人才。 这里的博客条目与开放职位的完整列表:)


    编辑

    请考虑Theo Yaung的回答。

    原始答案

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

    和阅读你的代码,我看到awaitFunc(int n)功能,这意味着从后代码await关键字,直到函数结束以后将作为代表来执行。

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

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

    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之前Func: Finally #X ,并且两者都应该出现在After the wait

    正如亨克霍尔特曼所解释的(有点突然),你在Func体内await的事实意味着之后的所有事情有时会被执行。

    没有解决方法,因为by design您可以在FuncBeginEnd之间进行await

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

    链接地址: http://www.djcxy.com/p/39521.html

    上一篇: Async CTP and "finally"

    下一篇: Task timeout question