これは素晴らしいキャッチです。実際に CTP にバグがあることに同意します。私はそれを掘り下げましたが、ここで何が起こっているのですか:
これは、非同期コンパイラ変換の CTP 実装と、.NET 4.0 以降の TPL (タスク並列ライブラリ) の既存の動作を組み合わせたものです。ここでの要因は次のとおりです。
- ソースからの finally 本体は、実際の CLR-finally 本体の一部に変換されます。これは多くの理由で望ましいことですが、そのうちの 1 つは、余分な時間をかけて例外をキャッチ/再スローすることなく、CLR に実行させることができることです。これにより、コード生成もある程度簡素化されます。コード生成が単純になると、コンパイル後のバイナリが小さくなり、多くのお客様がこれを望んでいます。:)
- メソッドの包括的な部分
Task
はFunc(int n)
、実際の TPL タスクです。に入るawait
とConsumer()
、メソッドの残りの部分は、返された fromConsumer()
の完了の続きとして実際にインストールされます。Task
Func(int n)
- CTP コンパイラが非同期メソッドを変換する方法により、実際のリターンの前に呼び出しに
return
マップされます。への呼び出しに要約されます。SetResult(...)
SetResult(...)
TaskCompletionSource<>.TrySetResult
TaskCompletionSource<>.TrySetResult
TPL タスクの完了を通知します。その継続が「いつか」発生するように即座に有効にします。この「いつか」は別のスレッドを意味する場合もあれば、状況によっては TPL がスマートで「ええと、この同じスレッドで今すぐ呼び出すこともできます」と言う場合もあります。
- 最終的に実行される直前に、包括的な
Task
ものは技術的に「完了」になります。Func(int n)
これは、非同期メソッドで待機していたコードが、並列スレッドで実行されるか、finally ブロックの前に実行される可能性があることを意味します。
包括的なTask
メソッドの非同期状態を表すことになっていることを考慮すると、基本的には、少なくともすべてのユーザー提供コードが言語設計に従って実行されるまで、完了としてフラグを立てるべきではありません。Anders、言語設計チーム、およびコンパイラ開発者にこの問題を提起して、これを見てもらいます。
症状の範囲/重大度:
通常、何らかのマネージド メッセージ ループが発生している WPF や WinForms のケースでは、これに悩まされることはありません。その理由は、await
onの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 では言語分野で採用を行っており、常に優秀な人材を探しています。募集職種の完全なリストを含むブログエントリはこちら:)