19

这是代码:

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

任何解决方法?

提前致谢!

4

2 回答 2

14

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

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

  1. 源代码中的 finally 主体被翻译成真正的 CLR-finally 主体的一部分。出于许多原因,这是可取的,其中之一是我们可以让 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. 在 finally 运行之前,最重要Task的 for在技术上已“完成”。Func(int n)这意味着等待异步方法的代码可能在并行线程中运行,甚至在 finally 块之前运行。

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


表现范围/严重性:

在 WPF 或 WinForms 情况下,您通常不会对此感到厌烦,因为您有某种托管消息循环正在进行。原因是awaitonTask实现遵循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。我知道这确实意味着您在代码流中失去了很多优雅。您可以使用异步辅助方法或辅助 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;
}

作为一个无耻的插件:我们正在微软的语言领域招聘,并且一直在寻找优秀的人才。此处的博客条目包含未平仓职位的完整列表 :)

于 2011-02-18T10:22:16.203 回答
2

编辑

请考虑 Theo Yaung 的回答

原始答案

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

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

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

因此,在到达代码in的那一刻,int u = await Func( i );行 inConsumer将继续执行。所以很有可能有:awaitFunc

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: EndandFunc: Finally可以出现在日志中的任何位置,唯一的限制是 a将Func: End #X出现在其关联的 之前Func: Finally #X,并且两者都应该出现在After the wait.

正如 Henk Holterman 所解释的那样(有点突然),你awaitFunc身体里放了一个事实意味着之后的一切都会在之后执行

没有解决方法,因为by designawait在 theBeginEndof之间放置了一个Func

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

于 2011-02-17T19:04:17.970 回答