简体   繁体   English

异步CTP和“终于”

[英]Async CTP and “finally”

Here's the code: 这是代码:

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

Here's the output: 这是输出:

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

As you can see, the finally block is executed much later then you'd expect. 正如您所看到的,finally块会在您预期的时间之后执行。

Any workarounds? 任何解决方法?

Thanks in advance! 提前致谢!

This is an excellent catch - and I agree that there is actually a bug in the CTP here. 这是一个很好的捕获 - 我同意这里的CTP实际上存在一个错误。 I dug into it and here's what's going on: 我挖了它,这是发生了什么:

This is a combination of the CTP implementation of the async compiler transformations, as well as the existing behavior of the TPL (Task Parallel Library) from .NET 4.0+. 这是异步编译器转换的CTP实现以及.NET 4.0+中TPL(任务并行库)的现有行为的组合。 Here are the factors at play: 以下是发挥作用的因素:

  1. The finally body from source is translated into part of a real CLR-finally body. 来自源的最终主体被翻译成真正的CLR最终主体的一部分。 This is desirable for many reasons, one of which is that we can get the CLR to execute it without catching/rethrowing the exception an extra time. 出于多种原因,这是可取的,其中一个原因是我们可以让CLR执行它而不会在额外的时间内捕获/重新抛出异常。 This also simplifies our code gen to some degree - simpler code gen results in smaller binaries once compiled, which is definitely desired by many of our customers. 这也在某种程度上简化了我们的代码生成 - 更简单的代码生成一旦编译就会产生更小的二进制文件,这是我们许多客户所希望的。 :) :)
  2. The overarching Task for the Func(int n) method is a real TPL task. Func(int n)方法的首要Task是一个真正的TPL任务。 When you await in Consumer() , then the rest of the Consumer() method is actually installed as a continuation off of the completion of the Task returned from Func(int n) . 当您在Consumer() await时,其余的Consumer()方法实际上是作为从Func(int n)返回的Task完成的继续安装的。
  3. The way the CTP compiler transforms async methods results in a return being mapped to a SetResult(...) call prior to a real return. CTP编译器转换异步方法的方式导致return在实际返回之前映射到SetResult(...)调用。 SetResult(...) boils down to a call to TaskCompletionSource<>.TrySetResult . SetResult(...)归结为对TaskCompletionSource<>.TrySetResult的调用TaskCompletionSource<>.TrySetResult
  4. TaskCompletionSource<>.TrySetResult signals the completion of the TPL task. TaskCompletionSource<>.TrySetResult表示TPL任务已完成。 Instantly enabling its continuations to occur "sometime". 立即使其延续“有时”发生。 This "sometime" may mean on another thread, or in some conditions the TPL is smart and says "um, I might as well just call it now on this same thread". 这个“某个时间”可能意味着在另一个线程上,或者在某些情况下TPL是聪明的并且说“嗯,我不妨现在在同一个线程上调用它”。
  5. The overarching Task for Func(int n) becomes technically "Completed" right before the finally gets run. 在最终运行之前, Func(int n)的首要Task在技​​术上变为“已完成”。 This means that code that was awaiting on an async method may run in parallel threads, or even before the finally block. 这意味着等待异步方法的代码可以在并行线程中运行,甚至可以在finally块之前运行。

Considering the overarching Task is supposed to represent the asynchronous state of the method, fundamentally it shouldn't get flagged as completed until at least all the user-provided code has been executed as per the language design. 考虑到总体Task应该代表方法的异步状态,从根本上说,它至少应该被标记为已完成,直到至少所有用户提供的代码都按照语言设计执行。 I'll bring this up with Anders, language design team, and compiler devs to get this looked at. 我将与Anders,语言设计团队和编译器开发人员一起讨论这个问题。


Scope of Manifestation / Severity: 表现范围/严重程度:

You typically won't be bit by this as bad in a WPF or WinForms case where you have some sort of managed message loop going on. 在WPF或WinForms中你通常会遇到某种托管消息循环,你通常不会觉得这很糟糕。 The reason why is that the await on Task implementations defer to the SynchronizationContext . 原因是Task实现上的await遵循SynchronizationContext This causes the async continuations to be queued up on the pre-existing message loop to be run on the same thread. 这会导致异步连续在预先存在的消息循环上排队,以便在同一个线程上运行。 You can verify this by changing your code to run Consumer() in the following way: 您可以通过以下方式更改代码以运行Consumer()来验证这一点:

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

Once run inside the context of the WPF message loop, the output appears as you would expect it: 一旦在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

Workaround: 解决方法:

Alas, the workaround means changing your code to not use return statements inside a try/finally block. 唉,解决方法意味着将代码更改为不在try/finally块中使用return语句。 I know this really means you lose a lot of elegance in your code flow. 我知道这真的意味着你在代码流中失去了很多优雅。 You can use async helper methods or helper lambdas to work around this. 您可以使用异步辅助方法或帮助程序lambdas来解决此问题。 Personally, I prefer the helper-lambdas because it automatically closes over locals/parameters from the containing method, as well as keeps your relevant code closer. 就个人而言,我更喜欢helper-lambdas,因为它会自动关闭包含方法的locals /参数,并使您的相关代码更接近。

Helper Lambda approach: 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;
}

Helper Method approach: 辅助方法方法:

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

As a shameless plug: We're hiring in the languages space at Microsoft, and always looking for great talent. 作为一个无耻的插件:我们正在微软的语言空间招聘,并一直在寻找优秀的人才。 Blog entry here with the full list of open positions :) 博客条目在这里有完整的未结头寸列表:)

Edit 编辑

Please consider Theo Yaung's answer . 请考虑Theo Yaung的答案

Original answer 原始答案

I'm not familiar with async/await, but after reading this: Visual Studio Async CTP Overview 我不熟悉async / await,但在阅读之后: Visual Studio异步CTP概述

and reading your code, I see the await in the Func(int n) function, meaning that from the code after the await keyword till the end of the function will be executed later as a delegate. 并且读取你的代码,我在Func(int n)函数中看到await ,这意味着从await关键字之后的代码到函数的结尾稍后将作为委托执行。

So my guess (and this is an uneducated guess) is that the Func:Begin and Func:End will possibly execute in different "contexts" (threads?), ie, asynchronously. 所以我的猜测 (这是一个没有受过教育的猜测)是Func:BeginFunc:End可能会在不同的“上下文”(线程?)中执行,即异步执行。

Thus, the int u = await Func( i ); 因此, int u = await Func( i ); line in Consumer will continue its execution the moment the code await in Func will be reached. Consumer中的行将在达到Func的代码await时继续执行。 So it is quite possible to have: 所以很有可能:

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

The Func: End and Func: Finally can appear in whatever position in the logs, the only constraint being that a Func: End #X will appear before its associated Func: Finally #X , and that both should appear before the After the wait . Func: EndFunc: Finally可以出现在日志中的任何位置,唯一的约束是Func: End #X将出现在其关联的Func: Finally #X ,并且两者都应该出现在After the wait

As explained (somewhat abruptly) by Henk Holterman is that the fact you put an await in the Func body means that everything after will be executed sometimes after. 正如Henk Holterman所解释的那样(有点突然),你在Func体中await的事实意味着之后的所有事情有时会被执行。

There is no workaround as, by design you put a await between the Begin and the End of Func . 没有解决方法,因为by design你在FuncBeginEnd之间进行了await

Just my uneducated 2 eurocents. 只是我未受过教育的2欧元。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM