非同期プログラミングを使用すると、パフォーマンスのボトルネックを回避し、アプリケーションの全体的な応答性を向上させることができます。 ただし、非同期アプリケーションを記述するための従来の手法は複雑になり、書き込み、デバッグ、保守が困難になる場合があります。
C# では、.NET ランタイムで非同期サポートを使用する、簡略化されたアプローチである非同期プログラミングがサポートされています。 コンパイラは開発者が行った困難な作業を行い、アプリケーションは同期コードに似た論理構造を保持します。 その結果、非同期プログラミングのすべての利点が少しの労力で得られます。
この記事では、非同期プログラミングを使用するタイミングと方法の概要について説明し、詳細と例を含む他の記事へのリンクを示します。
非同期により応答性が向上する
非同期性は、Web アクセスなど、ブロックされる可能性があるアクティビティに不可欠です。 Web リソースへのアクセスが遅くなったり、遅れたりすることがあります。 このようなアクティビティが同期プロセスでブロックされた場合、アプリケーション全体が待機する必要があります。 非同期プロセスでは、ブロックする可能性のあるタスクが完了するまで、Web リソースに依存しない他の作業をアプリケーションで続行できます。
次の表は、非同期プログラミングによって応答性が向上する一般的な領域を示しています。 .NET と Windows ランタイムの一覧に示されている API には、非同期プログラミングをサポートするメソッドが含まれています。
| アプリケーション領域 | 非同期メソッドを使用した .NET 型 | 非同期メソッドを使用した Windows ランタイム型 |
|---|---|---|
| Web アクセス | HttpClient | Windows.Web.Http.HttpClient SyndicationClient |
| ファイルの処理 | JsonSerializer StreamReader StreamWriter XmlReader XmlWriter |
StorageFile |
| 画像の操作 | MediaCapture BitmapEncoder BitmapDecoder |
|
| WCF プログラミング | 同期操作と非同期操作 |
すべての UI 関連アクティビティが通常 1 つのスレッドを共有するため、非同期性は UI スレッドにアクセスするアプリケーションにとって特に重要です。 同期アプリケーションでプロセスがブロックされると、すべてがブロックされます。 アプリケーションが応答を停止し、単に待機しているだけなのに、失敗したと誤解することがあります。
非同期メソッドを使用すると、アプリケーションは引き続き UI に応答します。 たとえば、ウィンドウのサイズを変更したり最小化したり、アプリケーションが終了するのを待たないようにする場合は、アプリケーションを閉じることができます。
非同期ベースのアプローチでは、非同期操作を設計するときに選択できるオプションの一覧に、自動転送と同等の機能が追加されます。 つまり、従来の非同期プログラミングのすべての利点を得ることができますが、開発者の労力ははるかに少なくなります。
非同期メソッドは簡単に記述できます
C# の async キーワードと await キーワードは、非同期プログラミングの中心です。 これらの 2 つのキーワードを使用すると、.NET Framework、.NET Core、または Windows ランタイムのリソースを使用して、同期メソッドを作成するのとほぼ同じくらい簡単に非同期メソッドを作成できます。
async キーワードを使用して定義する非同期メソッドは、非同期メソッドと呼ばれます。
非同期メソッドの例を次に示します。 コード内のほとんどすべてのものが、ユーザーにとってなじみのあるものになるはずです。
完全な Windows Presentation Foundation (WPF) の例は、 C# で async と await を使用した非同期プログラミングからダウンロードできます。
public async Task<int> GetUrlContentLengthAsync()
{
using var client = new HttpClient();
Task<string> getStringTask =
client.GetStringAsync("https://learn.microsoft.com/dotnet");
DoIndependentWork();
string contents = await getStringTask;
return contents.Length;
}
void DoIndependentWork()
{
Console.WriteLine("Working...");
}
前のサンプルからいくつかのプラクティスを学習できます。 メソッド シグネチャから始めます。
async修飾子が含まれています。 戻り値の型は Task<int> です (その他のオプションについては、「戻り値の型」セクションを参照してください)。 メソッド名は Asyncで終わります。 メソッドの本体で、 GetStringAsync は Task<string>を返します。 つまり、タスクを await すると、 string (contents) が表示されます。 タスクを待機する前に、stringからのGetStringAsyncに依存しない作業を実行できます。
await演算子に細心の注意を払います。
GetUrlContentLengthAsyncを中断します。
-
GetUrlContentLengthAsyncは、getStringTaskが完了するまで続行できません。 - その間、コントロールは
GetUrlContentLengthAsyncの呼び出し元に戻されます。 -
getStringTaskが完了すると、ここで制御が再開されます。 - 次に、
await演算子は、stringからgetStringTask結果を取得します。
return ステートメントは、整数の結果を指定します。
GetUrlContentLengthAsync を待機しているメソッドは、長さの値を取得します。
GetUrlContentLengthAsync
GetStringAsyncの呼び出しとその完了の待機の間に実行できる作業がない場合は、次の単一ステートメントを呼び出して待機することで、コードを簡略化できます。
string contents = await client.GetStringAsync("https://learn.microsoft.com/dotnet");
次の特性は、前の例を非同期メソッドにする内容をまとめたものです。
メソッド シグネチャには、
async修飾子が含まれています。非同期メソッドの名前は、慣例により末尾に "Async" サフィックスを付けます。
戻り値の型は、次のいずれかの型です。
-
Task<TResult> メソッドに、オペランドに型が
TResultを持つ return ステートメントがある場合。 - Task メソッドに return ステートメントがない場合、またはオペランドのない return ステートメントがある場合。
-
void非同期イベント ハンドラーを記述している場合は 。 -
GetAwaiterメソッドがあるその他の型。
詳細については、「 戻り値の型とパラメーター 」セクションを参照してください。
-
Task<TResult> メソッドに、オペランドに型が
通常、このメソッドには少なくとも 1 つの
await式が含まれています。これは、待機中の非同期操作が完了するまでメソッドを続行できないポイントをマークします。 その間、メソッドは中断され、コントロールはメソッドの呼び出し元に戻ります。 この記事の次のセクションでは、中断ポイントで何が起こるかを示します。
非同期メソッドでは、指定されたキーワードと型を使用して何を行うかを示し、中断されたメソッドの待機ポイントに制御が戻ったときに何が起こるかを追跡するなど、コンパイラが残りの処理を行います。 ループや例外処理などの一部のルーチン プロセスは、従来の非同期コードでは処理が困難な場合があります。 非同期メソッドでは、同期ソリューションの場合と同じようにこれらの要素を記述すると、問題が解決されます。
以前のバージョンの .NET Framework での非同期性の詳細については、 TPL と従来の .NET Framework 非同期プログラミングを参照してください。
非同期メソッドで何が起こるか
非同期プログラミングで理解しておく必要がある最も重要なことは、制御フローがメソッドからメソッドにどのように移行されるかです。 次の図は、プロセスを示しています。
図の番号は、呼び出し元のメソッドが非同期メソッドを呼び出したときに開始される次の手順に対応しています。
呼び出し元のメソッドは、
GetUrlContentLengthAsync非同期メソッドを呼び出して待機します。GetUrlContentLengthAsyncは、 HttpClient インスタンスを作成し、 GetStringAsync 非同期メソッドを呼び出して、Web サイトの内容を文字列としてダウンロードします。進行状況を中断する
GetStringAsyncで何かが発生します。 おそらく、Web サイトのダウンロードやその他のブロックアクティビティを待つ必要があります。 リソースのブロックを回避するために、GetStringAsyncは呼び出し元のGetUrlContentLengthAsyncに制御を渡します。GetStringAsyncは Task<TResult>を返します。ここで、TResultは文字列であり、GetUrlContentLengthAsyncはタスクをgetStringTask変数に割り当てます。 タスクは、GetStringAsyncの呼び出しの進行中のプロセスを表し、作業が完了したときに実際の文字列値を生成することを約束します。getStringTaskはまだ待機していないため、GetUrlContentLengthAsyncは、GetStringAsyncの最終結果に依存しない他の作業を続行できます。 この作業は、同期メソッドDoIndependentWorkの呼び出しによって表されます。DoIndependentWorkは、その処理を行い、呼び出し元に戻る同期メソッドです。GetUrlContentLengthAsyncgetStringTaskの結果なしで実行できる作業が不足しています。GetUrlContentLengthAsync次に、ダウンロードした文字列の長さを計算して返しますが、メソッドが文字列を持つまで、メソッドはその値を計算できません。そのため、
GetUrlContentLengthAsyncは await 演算子を使用して進行状況を中断し、GetUrlContentLengthAsyncを呼び出したメソッドに制御を与えます。GetUrlContentLengthAsyncは、呼び出し元にTask<int>を返します。 タスクは、ダウンロードした文字列の長さである整数の結果を生成する約束を表します。注
GetStringAsyncが待機する前にgetStringTask(したがってGetUrlContentLengthAsync) が完了した場合、制御はGetUrlContentLengthAsyncに残ります。 呼び出された非同期プロセスが完了し、最終結果を待つ必要がない場合、中断してからGetUrlContentLengthAsyncに戻る費用は無駄になります。呼び出し元のメソッド内では、処理パターンが続行されます。 呼び出し元は、その結果を待機する前に、
GetUrlContentLengthAsyncの結果に依存しない他の作業を行うか、呼び出し元がすぐに待機する可能性があります。 呼び出し元のメソッドはGetUrlContentLengthAsyncを待機しており、GetUrlContentLengthAsyncはGetStringAsyncを待機しています。GetStringAsyncが完了し、文字列の結果が生成されます。 文字列の結果は、期待した方法でGetStringAsyncを呼び出しても返されません。 (このメソッドは、手順 3 で既にタスクを返したことを思い出してください)。代わりに、文字列の結果は、メソッドの完了を表すタスクに格納getStringTask。 await 演算子は、getStringTaskから結果を取得します。 assignment ステートメントは、取得した結果をcontentsに割り当てます。GetUrlContentLengthAsyncに文字列の結果がある場合、メソッドは文字列の長さを計算できます。 その後、GetUrlContentLengthAsyncの作業も完了し、待機中のイベント ハンドラーを再開できます。 記事の最後の完全な例では、イベント ハンドラーが長さの結果の値を取得して出力することを確認できます。 非同期プログラミングを初めて使用する場合は、同期動作と非同期動作の違いを考慮する必要があります。 同期メソッドは、作業が完了すると返されますが (手順 5)、非同期メソッドは、作業が中断されたときにタスク値を返します (手順 3 と 6)。 非同期メソッドが最終的にその作業を完了すると、タスクは完了としてマークされ、結果がある場合はタスクに格納されます。
API 非同期メソッド
非同期プログラミングをサポートする GetStringAsync などのメソッドがどこにあるか疑問に思うかもしれません。 .NET Framework 4.5 以降と .NET Core には、 async と awaitで動作する多くのメンバーが含まれています。 メンバー名に追加された "Async" サフィックス、および戻り値の型の Task または Task<TResult>で認識できます。 たとえば、System.IO.Stream クラスには、CopyToAsync、ReadAsync、およびWriteAsyncの同期メソッドと共に、CopyTo、Read、Writeなどのメソッドが含まれます。
Windows ランタイムには、Windows アプリの async と await で使用できる多くのメソッドも含まれています。 詳細については、「UWP 開発用 のスレッドと非同期プログラミング 」および 「非同期プログラミング (Windows ストア アプリ)」 および「クイック スタート: 以前のバージョンの Windows ランタイムを使用する場合は 、C# または Visual Basic で非同期 API を呼び出 す」を参照してください。
スレッド数
非同期メソッドは、非ブロッキング操作を目的としています。 非同期メソッドの await 式は、待機中のタスクの実行中に現在のスレッドをブロックしません。 代わりに、式はメソッドの残りの部分を継続としてサインアップし、非同期メソッドの呼び出し元に制御を返します。
asyncキーワードとawaitキーワードでは、追加のスレッドは作成されません。 非同期メソッドは独自のスレッドで実行されないため、非同期メソッドではマルチスレッドは必要ありません。 メソッドは現在の同期コンテキストで実行され、メソッドがアクティブな場合にのみスレッドで時間を使用します。
Task.Runを使用して CPU バインド作業をバックグラウンド スレッドに移動できますが、バックグラウンド スレッドは結果が使用可能になるのを待っているだけのプロセスには役立ちません。
非同期プログラミングに対する非同期ベースのアプローチは、ほぼすべてのケースで既存のアプローチに適しています。 特に、この方法は I/O バインド操作の BackgroundWorker クラスよりも優れています。これは、コードが単純であり、競合状態から保護する必要がないためです。
Task.Runメソッドと組み合わせることで、非同期プログラミングは CPU バインド操作のBackgroundWorkerよりも優れています。非同期プログラミングでは、コードの実行に関する調整の詳細がスレッド プールに転送Task.Run作業から分離されるためです。
Asyncとawait
非同期 修飾子を 使用してメソッドが非同期メソッドであることを指定する場合は、次の 2 つの機能を有効にします。
マークされた非同期メソッドは、 await を使用して中断ポイントを指定できます。
await演算子は、待機中の非同期プロセスが完了するまで非同期メソッドがその時点を超えて続行できないことをコンパイラに通知します。 その間、コントロールは非同期メソッドの呼び出し元に戻ります。await式での非同期メソッドの中断はメソッドからの終了を構成せず、finallyブロックは実行されません。マークされた非同期のメソッド自体は、呼び出し元のメソッドによって待機できます。
非同期メソッドには通常、 await 演算子が 1 回以上出現しますが、 await 式がない場合はコンパイラ エラーは発生しません。 非同期メソッドが await 演算子を使用して中断ポイントをマークしない場合、メソッドは、 async 修飾子にもかかわらず同期メソッドとして実行されます。 コンパイラは、このようなメソッドに対して警告を発行します。
async と await はコンテキスト キーワードです。 詳細と例については、次の記事を参照してください。
戻り値の型とパラメーター
非同期メソッドは、通常、 Task または Task<TResult>を返します。 非同期メソッド内では、 await 演算子が、別の非同期メソッドの呼び出しから返されるタスクに適用されます。
メソッドにTask<TResult>型のオペランドを指定するreturn ステートメントが含まれている場合は、戻り値の型としてTResultを指定します。
メソッドに return ステートメントがない場合、またはオペランドを返さない return ステートメントがある場合は、戻り値の型として Task を使用します。
型に GetAwaiter メソッドが含まれている場合は、その他の戻り値の型を指定することもできます。
ValueTask<TResult> は、このような型の例です。
System.Threading.Tasks.Extension NuGet パッケージで使用できます。
次の例は、 Task<TResult> または Taskを返すメソッドを宣言して呼び出す方法を示しています。
async Task<int> GetTaskOfTResultAsync()
{
int hours = 0;
await Task.Delay(0);
return hours;
}
Task<int> returnedTaskTResult = GetTaskOfTResultAsync();
int intResult = await returnedTaskTResult;
// Single line
// int intResult = await GetTaskOfTResultAsync();
async Task GetTaskAsync()
{
await Task.Delay(0);
// No return statement needed
}
Task returnedTask = GetTaskAsync();
await returnedTask;
// Single line
await GetTaskAsync();
返される各タスクは、進行中の作業を表します。 タスクは、非同期プロセスの状態に関する情報をカプセル化し、最終的には、プロセスの最終的な結果またはプロセスが成功しなかった場合に発生する例外をカプセル化します。
非同期メソッドには、 void 戻り値の型を指定することもできます。 この戻り値の型は、主に、 void 戻り値の型が必要なイベント ハンドラーを定義するために使用されます。 非同期イベント ハンドラーは、多くの場合、非同期プログラムの開始点として機能します。
void の戻り値の型を持つ非同期のメソッドは、待機できません。void を戻すメソッドの呼び出し元では、このメソッドがスローする例外をキャッチできません。
非同期メソッドは in、ref、または out パラメーターを宣言できませんが、このようなパラメーターを持つメソッドを呼び出すことができます。 同様に、非同期メソッドは参照によって値を返すことはできませんが、ref 戻り値を持つメソッドを呼び出すことができます。
詳細と例については、「 非同期の戻り値の型 (C#)」を参照してください。
Windows ランタイム プログラミングの非同期 API には、タスクに似た次のいずれかの戻り値の型があります。
- IAsyncOperation<TResult>に対応します。 Task<TResult>
- IAsyncActionに対応します。 Task
- IAsyncActionWithProgress<TProgress>
- IAsyncOperationWithProgress<TResult,TProgress>
名前付け規則
慣例により、一般的に待機可能な型 ( Task、 Task<T>、 ValueTask、 ValueTask<T>など) を返すメソッドには、"Async" で終わる名前が必要です。 非同期操作を開始するが待機可能な型を返さないメソッドは、"Async" で終わる名前を持つべきではありませんが、"Begin"、"Start"、またはその他の動詞で始まり、このメソッドが操作の結果を返したりスローしたりしないことを示唆する場合があります。
イベント、基底クラス、またはインターフェイス コントラクトが別の名前を提案する規則は無視できます。 たとえば、 OnButtonClickなどの一般的なイベント ハンドラーの名前を変更しないでください。
関連記事 (Visual Studio)
| タイトル | 説明 |
|---|---|
| async と await を使用して複数の Web 要求を並列に行う方法 (C#) | 複数のタスクを同時に開始する方法を示します。 |
| 非同期戻り値型 (C#) | 非同期メソッドが返すことができる型を示し、各型が適切な場合について説明します。 |
| 通知メカニズムとしてキャンセル トークンを使用してタスクを取り消します。 | 非同期ソリューションに次の機能を追加する方法を示します。 - タスクの一覧を取り消す (C#) - 一定期間後のタスクの取り消し (C#) - 完了時に非同期タスクを処理する (C#) |
| ファイル アクセスに async を使用する (C#) | async と await を使用してファイルにアクセスする利点を示します。 |
| タスク ベースの非同期パターン (TAP) | 非同期パターンについて説明します。 パターンは、 Task 型と Task<TResult> 型に基づいています。 |
| チャネル 9 の非同期ビデオ | 非同期プログラミングに関するさまざまなビデオへのリンクを提供します。 |
こちらも参照ください
.NET