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 本体の一部に変換されます。これは多くの理由で望ましいことですが、そのうちの 1 つは、余分な時間をかけて例外をキャッチ/再スローすることなく、CLR に実行させることができることです。これにより、コード生成もある程度簡素化されます。コード生成が単純になると、コンパイル後のバイナリが小さくなり、多くのお客様がこれを望んでいます。:)
  2. メソッドの包括的な部分TaskFunc(int n)、実際の TPL タスクです。に入るawaitConsumer()、メソッドの残りの部分は、返された fromConsumer()の完了の続きとして実際にインストールされます。TaskFunc(int n)
  3. CTP コンパイラが非同期メソッドを変換する方法により、実際のリターンの前に呼び出しにreturnマップされます。への呼び出しに要約されます。SetResult(...)SetResult(...)TaskCompletionSource<>.TrySetResult
  4. TaskCompletionSource<>.TrySetResultTPL タスクの完了を通知します。その継続が「いつか」発生するように即座に有効にします。この「いつか」は別のスレッドを意味する場合もあれば、状況によっては TPL がスマートで「ええと、この同じスレッドで今すぐ呼び出すこともできます」と言う場合もあります。
  5. 最終的に実行される直前に、包括的なTaskものは技術的に「完了」になります。Func(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);

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これは、コード フローで多くのエレガンスが失われることを意味します。これを回避するには、非同期ヘルパー メソッドまたはヘルパー ラムダを使用できます。個人的には、ヘルパー ラムダの方が好きです。これは、含まれているメソッドから自動的にローカル/パラメーターを閉じ、関連するコードを近くに保つためです。

ヘルパー ラムダ アプローチ:

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

恥知らずなプラグインとして: Microsoft では言語分野で採用を行っており、常に優秀な人材を探しています。募集職種の完全なリストを含むブログエントリはこちら:)

于 2011-02-18T10:22:16.203 に答える
2

編集

Theo Yaung の回答を検討してください。

元の答え

私は async/await に慣れていませんが、これを読んだ後: Visual Studio Async CTP Overview

コードを読むとawait、関数内に が表示されます。つまり、キーワードのFunc(int n)コードから関数の最後までが、後でデリゲートとして実行されます。await

したがって、私の推測(これは根拠のない推測ですが) と は、異なる「コンテキスト」(スレッド?)、つまり非同期で実行される可能性がありますFunc:BeginFunc: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: EndFunc: Finallyは、ログのどの位置にも表示できます。唯一の制約は、 が関連付けられたのFunc: End #X前に表示さFunc: Finally #Xれ、両方が の前に表示される必要があることAfter the waitです。

awaitHenk Holterman が (やや唐突に) 説明したように、ボディに を入れるということは、Funcその後のすべてが時々実行されることを意味するということです。

との間by designに a を入れるので、回避策はありません。awaitBeginEndFunc

私の教育を受けていない2ユーロセントだけです。

于 2011-02-17T19:04:17.970 に答える