【问题标题】:Async CTP and "finally"异步 CTP 和“终于”
【发布时间】:2011-02-17 18:07:48
【问题描述】:

代码如下:

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 块的执行时间比您预期的要晚很多

有什么解决方法吗?

提前致谢!

【问题讨论】:

  • 看看反射器生成的 C# 会很有趣,看看编译器生成的状态机是什么样子的。这就是答案所在。
  • @btlog:我做到了。它将我的“return 0”转换为某个“SetResult()”调用,该调用在同一个线程上从内部调用“Consumer”的等待块。有趣的是异常,“finally”块在它应该执行的时候执行,即控件离开“Func”之前。
  • 这是一个很棒的发现 - 组成我的答案:)

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


【解决方案1】:

这是一个很好的发现——我同意这里的 CTP 实际上存在一个错误。我深入研究了它,这是发生了什么:

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

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

考虑到最重要的 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

解决方法:

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

辅助 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

阅读您的代码后,我在Func(int n) 函数中看到了await,这意味着从代码之后 到函数末尾的await 关键字将在稍后执行为一个代表。

所以我的猜测(这是一个未经证实的猜测)是Func:BeginFunc:End 可能会在不同的“上下文”(线程?)中执行,即异步执行。

因此,Consumer 中的int u = await Func( i ); 行将在到达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 之前,并且两者都应该出现在After the wait 之前。

正如 Henk Holterman 所解释的(有点突然),您在 Func 正文中添加 await 意味着之后的所有内容有时会在之后执行。

没有解决方法,因为by designBeginFuncEnd 之间放置了一个await

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

【讨论】:

    猜你喜欢
    • 2011-08-19
    • 1970-01-01
    • 1970-01-01
    • 2012-02-05
    • 2011-12-10
    • 1970-01-01
    • 2011-09-20
    • 2011-10-20
    • 1970-01-01
    相关资源
    最近更新 更多