C#5.0で導入されたasyncおよびawaitキーワードは、非同期プログラミングを大幅に簡素化します。 また、注意を怠ると、コードに問題が生じる可能性のあるいくつかの困難も隠されます。 .NETアプリケーション用の非同期コードを作成する場合、以下に説明するプラクティスが役立ちます。
async / awaitは、長続きする場所にのみ使用してください
ここではすべてが簡単です。 非同期操作を管理するための
Task
およびその他の構造を作成すると、オーバーヘッドが追加されます。 IO要求の実行など、操作が本当に長い場合、これらのコストは主に目立たなくなります。 また、操作が短い場合、または複数のプロセッササイクルが必要な場合は、この操作を同期的に実行することをお勧めします。
一般に、.NET Frameworkに取り組んでいるチームは、非同期である必要がある機能を選択するという非常に良い仕事をしました。 そのため、フレームワークメソッドが
Async
で終了してタスクを返す場合、おそらく非同期で使用する必要があります。
タスクよりも非同期/待機を優先する
async/await
を使用して非同期コードを記述すると、
Task
タスクを使用するのではなく、コードの作成と読み取りの両方のプロセスが大幅に簡素化されます。
public Task<Data> GetDataAsync() { return MyWebService.FetchDataAsync() .ContinueWith(t => new Data (t.Result)); }
public async Task<Data> GetDataAsync() { var result = await MyWebService.FetchDataAsync(); return new Data (result); }
パフォーマンスの点では、上記の両方の方法のオーバーヘッドはわずかですが、それらのタスクの数が増えると、スケーリングが多少異なります。
-
Task
は、連続して接続されたタスクの数に応じて増加する継続チェーンを構築します。システムの状態は、コンパイラーが検出したクロージャーによって制御されます。 -
Async/await
は、新しいステップを追加するときに追加のリソースを使用しない状態マシンを構築します。 ただし、コンパイラーは、コード(およびコンパイラー)に応じて、マシンのスタックに状態を格納するための変数をさらに定義できます。 MSDNの記事には、何が起きているかについての優れた詳細があります。
ほとんどのシナリオでは、
async/await
は使用するリソースが少なく、
Task
タスクよりも速く実行されます。
条件コードに既に完了した空の静的タスクを使用する
ある条件下でのみタスクを実行したい場合があります。 残念ながら、
await
はタスクの代わりに
null
を取得
NullReferenceException
を
NullReferenceException
それを処理するとコードが読みにくくなります。
public async Task<Data> GetDataAsync(bool getLatestData) { Task<WebData> task = null; if (getLatestData) task = MyWebService.FetchDataAsync(); // // null WebData result = null; if (task != null) result = await task; return new Data (result); }
コードを少し単純化する1つの方法は、すでに完了している空のタスクを使用することです。 結果のコードはよりきれいになります:
public async Task<Data> GetDataAsync(bool getLatestData) { var task = getLatestData ? MyWebService.FetchDataAsync() : Empty<WebData>.Task; // // task null return new Data (await task); }
タスクが静的であり、完了時に作成されていることを確認してください。 例:
public static class Empty<T> { public static Task<T> Task { get { return _task; } } private static readonly Task<T> _task = System.Threading.Tasks.Task.FromResult(default(T)); }
パフォーマンス:データよりもタスク自体をキャッシュすることを好む
タスクを作成する際にオーバーヘッドが発生します。 結果をキャッシュしてからタスクに戻す場合、追加のタスクオブジェクトを作成できます。
public Task<byte[]> GetContentsOfUrl(string url) { byte[] bytes; if (_cache.TryGetValue(url, out bytes)) // return Task<byte[]>.Factory.StartNew(() => bytes); bytes = MyWebService.GetContentsAsync(url) .ContinueWith(t => { _cache.Add(url, t.Result); return t.Result; ); } // ( ) private static Dictionary<string, byte[]> _cache = new Dictionary<string, byte[]>();
代わりに、タスク自体をキャッシュする方が良いでしょう。 この場合、それらを使用するコードは、既に完了したタスクを待つことができます。 タスク並列ライブラリには最適化があり、すでに完了したタスクの完了を待機しているコードがより高速に実行されます 。
public Task<byte[]> GetContentsOfUrl(string url) { Task<byte[]> bytes; if (!_cache.TryGetValue(url, out bytes)) { bytes = MyWebService.GetContentsAsync(url); _cache.Add(url, bytes); } return bytes; } // ( ) private static Dictionary<string, Task<byte[]>> _cache = new Dictionary<string, Task<byte[]>>();
パフォーマンス:待機が状態を維持する方法を理解する
async/await
を使用すると、コンパイラは変数とスタックを保存するステートマシンを作成します。 例:
public static async Task FooAsync() { var data = await MyWebService.GetDataAsync(); var otherData = await MyWebService.GetOtherDataAsync(); Console.WriteLine("{0} = "1", data, otherdata); }
これにより、複数の変数を持つ状態オブジェクトが作成されます。 コンパイラーがメソッド変数を保存する方法を参照してください。
[StructLayout(LayoutKind.Sequential), CompilerGenerated] private struct <FooAsync>d__0 : <>t__IStateMachine { private int <>1__state; public AsyncTaskMethodBuilder <>t__builder; public Action <>t__MoveNextDelegate; public Data <data>5__1; public OtherData <otherData>5__2; private object <>t__stack; private object <>t__awaiter; public void MoveNext(); [DebuggerHidden] public void <>t__SetMoveNextDelegate(Action param0); }
備考1.変数を宣言すると、変数は状態を保存するオブジェクトに保存されます。 これにより、オブジェクトが予想よりも長くメモリに残る可能性があります。
注2.ただし、変数を宣言せず、呼び出しの
Async
値と
await
と、変数は内部スタックに移動します。
public static async Task FooAsync() { var data = MyWebService.GetDataAsync(); var otherData = MyWebService.GetOtherDataAsync(); // // await- Console.WriteLine("{0} = "1", await data, await otherdata); }
パフォーマンスの問題が発生するまで、これについて心配する必要はありません。 最適化をさらに深くすることに決めた場合は、MSDNでこれに関する良い記事があります: Async Performance:Understanding the Costs of Async and Await 。
安定性:async / awaitはTask.Waitではありません
async/await
によって生成される状態マシンは、
Task.ContinueWith/Wait
とは
Task.ContinueWith/Wait
ます。 一般的な場合、実装を
Task
から
await
に置き換えることができますが、パフォーマンスと安定性の問題が発生する可能性があります。 もっと詳しく見てみましょう。
安定性:同期コンテキストを知る
.NETコードは常に何らかのコンテキストで実行されます。 このコンテキストは、現在のユーザーと、フレームワークに必要な他の値を定義します。 一部の実行コンテキストでは、コードは同期のコンテキストで機能し、タスクおよびその他の非同期作業の実行を制御します。
デフォルトでは、
await
後
await
コードは実行されたコンテキストで引き続き動作します。 これは、基本的にセキュリティコンテキストを復元し、起動時にコードに既にアクセスしているWindows UIオブジェクトにアクセスできるようになるまで待機する必要があるため、便利です。
Task.Factory.StartNew
はコンテキストを復元しないことに注意してください。
一部の同期コンテキストは、それらへの再入力をサポートせず、シングルスレッドです。 つまり、このコンテキストでは一度に1つの作業単位しか実行できません。 この例は、Windows UIスレッドまたはASP.NETコンテキストです。
このようなシングルスレッドの同期コンテキストでは、デッドロックを取得するのは非常に簡単です。 シングルスレッドコンテキストでタスクを作成し、同じコンテキストで待機すると、待機中のコードがバックグラウンドタスクの実行をブロックします。
public ActionResult ActionAsync() { // DEADLOCK: // , var data = GetDataAsync().Result; return View(data); } private async Task<string> GetDataAsync() { // var result = await MyWebService.GetDataAsync(); return result.ToString(); }
安定性:タスクがここで完了するのを待つために待機を使用しないで
Wait
一般的なルールとして、非同期コードを作成する場合は、
Wait
使用に注意して
Wait
。 (c
await
が少しいいです。)
次のようなシングルスレッド同期コンテキストでタスクの
Wait
を使用しないでください。
- UIスレッド
- ASP.NETコンテキスト
幸いなことに、フレームワークでは特定の場合に
Task
を返すことができ、フレームワーク自体はタスクが完了するまで待機します。 彼にこのプロセスを信頼してください:
public async Task<ActionResult> ActionAsync() { // async/await Task var data = await GetDataAsync(); return View(data); }
非同期ライブラリを作成する場合、ユーザーは非同期コードを記述する必要があります。 非同期コードの記述は退屈でエラーが発生しやすいため、これは問題でしたが、
async/await
登場により
async/await
ほとんどの複雑さはコンパイラーによって処理されるようになりました。 また、コードの信頼性が向上し、現在では
ThreadPool
の微妙な
ThreadPool
に対処する必要が少なくなります。
安定性:ライブラリを作成する場合は
ConfigureAwait
使用を検討してください
これらのコンテキストのいずれかでタスクが完了するまで待機する必要がある場合、
ConfigureAwait
を使用して、コンテキストでバックグラウンドタスクを実行しないようにシステムに指示できます。 この短所は、バックグラウンドタスクが同じ同期コンテキストにアクセスできないため、Windows UIまたは
HttpContext
へのアクセスが失われることです(ただし、セキュリティコンテキストはまだあります)。
Task
を返す「ライブラリ」関数を作成する場合、おそらくどのように呼び出されるかわかりません。 そのため、タスクを返す前に
ConfigureAwait(false)
をタスクに追加する方が安全かもしれません。
private async Task<string> GetDataAsync() { // ConfigureAwait(false) , // var result = await MyWebService.GetDataAsync().ConfigureAwait(false); return result.ToString(); }
安定性:例外の動作を理解します。
非同期コードを見ると、例外がどうなるかを言うのが難しい場合があります。 それは呼び出し関数に渡されるのでしょうか、それともタスクの完了を待っているコードに渡されるのでしょうか?
この場合のルールは非常に簡単ですが、コードを見ただけでは質問に答えることがまだ難しい場合があります。
いくつかの例:
- async / awaitメソッド自体からスローされた例外は、タスクの完了を待機するコード(awaiter)に送信されます。
public async Task<Data> GetContentsOfUrl(string url) { // , // if (url == null) throw new ArgumentNullException(); var data = await MyWebService.GetContentsOfUrl(); return data.DoStuffToIt(); }
-
Task
タスクのデリゲートからスローされた例外も、タスクの完了を待機するコード(awaiter)に送信されます。
public Task<Data> GetContentsOfUrl(string url) { return Task<Data>.Factory.StartNew(() => { // , // if (url == null) throw new ArgumentNullException(); var data = await MyWebService.GetContentsOfUrl(); return data.DoStuffToIt(); } }
- タスクの作成中にスローされた例外は、このメソッドを呼び出したコード(呼び出し元)に送信されます(一般的には明らかです)。
public Task<Data> GetContentsOfUrl(string url) { // if (url == null) throw new ArgumentNullException(); return Task<Data>.Factory.StartNew(() => { var data = await MyWebService.GetContentsOfUrl(); return data.DoStuffToIt(); } }
最後の例は、
Task
介して
Task
チェーンを作成するよりも
async/await
を好む理由の1つです。
追加のリンク (英語)
- MSDN: Async / Await FAQ
- await“ fast track ”最適化について
- MSDN: 待って、UI、そしてデッドロック! ああ!
- MSDN: 非同期パフォーマンス:非同期と待機のコストを理解する