C#でasyncとawaitを使用する-ベストプラクティス


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

パフォーマンスの点では、上記の両方の方法のオーバーヘッドはわずかですが、それらのタスクの数が増えると、スケーリングが多少異なります。

ほとんどのシナリオでは、 async/awaitは使用するリソースが少なく、 Taskタスクよりも速く実行されます。

条件コードに既に完了した空の静的タスクを使用する

ある条件下でのみタスクを実行したい場合があります。 残念ながら、 awaitはタスクの代わりにnullを取得NullReferenceExceptionNullReferenceExceptionそれを処理するとコードが読みにくくなります。

 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コードは常に何らかのコンテキストで実行されます。 このコンテキストは、現在のユーザーと、フレームワークに必要な他の値を定義します。 一部の実行コンテキストでは、コードは同期のコンテキストで機能し、タスクおよびその他の非同期作業の実行を制御します。

デフォルトでは、 awaitawaitコードは実行されたコンテキストで引き続き動作します。 これは、基本的にセキュリティコンテキストを復元し、起動時にコードに既にアクセスしている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を使用しないでください。

幸いなことに、フレームワークでは特定の場合に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(); } 


安定性:例外の動作を理解します。

非同期コードを見ると、例外がどうなるかを言うのが難しい場合があります。 それは呼び出し関数に渡されるのでしょうか、それともタスクの完了を待っているコードに渡されるのでしょうか?

この場合のルールは非常に簡単ですが、コードを見ただけでは質問に答えることがまだ難しい場合があります。

いくつかの例:

最後の例は、 Task介してTaskチェーンを作成するよりもasync/awaitを好む理由の1つです。

追加のリンク (英語)

Source: https://habr.com/ru/post/J162353/


All Articles