ネットワーク データ要求、データベース アクセス、またはファイル システムの読み取り/書き込みをサポートする I/O バインド シナリオをコードで実装する場合は、非同期プログラミングが最適な方法です。 また、高価な計算などの CPU バインド シナリオの非同期コードを記述することもできます。
C# には言語レベルの非同期プログラミング モデルがあり、コールバックを処理したり、非同期をサポートするライブラリに準拠したりする必要なく、非同期コードを簡単に記述できます。 このモデルは、 タスク ベースの非同期パターン (TAP) と呼ばれるものに従います。
非同期プログラミング モデルを調べる
Task
オブジェクトとTask<T>
オブジェクトは、非同期プログラミングの中核を表します。 これらのオブジェクトは、 async
キーワードと await
キーワードをサポートすることで非同期操作をモデル化するために使用されます。 ほとんどの場合、モデルは I/O バインドと CPU バインドの両方のシナリオで非常に単純です。
async
メソッド内:
-
I/O バインド コードは、
Task
メソッド内のTask<T>
またはasync
オブジェクトによって表される操作を開始します。 - CPU バインド コードは 、 Task.Run メソッドを使用してバックグラウンド スレッドで操作を開始します。
どちらの場合も、アクティブな Task
は、完了していない可能性がある非同期操作を表します。
await
キーワードはマジックが行われる場所であり、
await
式を含むメソッドの呼び出し元に制御を渡し、最終的には UI の応答性を高めるか、サービスをエラスティックにすることができます。
式とasync
式を使用する以外に非同期コードにアプローチするawait
が、この記事では言語レベルのコンストラクトについて説明します。
注
この記事で紹介する例の一部では、 System.Net.Http.HttpClient クラスを使用して Web サービスからデータをダウンロードします。 コード例では、 s_httpClient
オブジェクトはクラス Program
型の静的フィールドです。
private static readonly HttpClient s_httpClient = new();
詳細については、この記事の最後にある 完全なコード例 を参照してください。
基になる概念を確認する
C# コードで非同期プログラミングを実装すると、コンパイラによってプログラムがステート マシンに変換されます。 このコンストラクトは、コードが await
式に達したときに実行を生成したり、バックグラウンド ジョブが完了したときに実行を再開するなど、コード内のさまざまな操作と状態を追跡します。
コンピューター サイエンス理論では、非同期プログラミングは非同期の Promise モデルの実装です。
非同期プログラミング モデルでは、いくつかの重要な概念を理解する必要があります。
- I/O バインド コードと CPU バインド コードの両方に非同期コードを使用できますが、実装は異なります。
- 非同期コードでは、バックグラウンドで実行されている作業をモデル化するために、
Task<T>
オブジェクトとTask
オブジェクトをコンストラクトとして使用します。 -
async
キーワードは、メソッドを非同期メソッドとして宣言します。これにより、メソッド本体でawait
キーワードを使用できます。 -
await
キーワードを適用すると、コードは呼び出し元メソッドを中断し、タスクが完了するまで制御を呼び出し元に戻します。 -
await
式は、非同期メソッドでのみ使用できます。
I/O バインドの例: Web サービスからデータをダウンロードする
この例では、ユーザーがボタンを選択すると、アプリは Web サービスからデータをダウンロードします。 ダウンロード プロセス中にアプリの UI スレッドをブロックしたくない。 次のコードは、このタスクを実行します。
s_downloadButton.Clicked += async (o, e) =>
{
// This line will yield control to the UI as the request
// from the web service is happening.
//
// The UI thread is now free to perform other work.
var stringData = await s_httpClient.GetStringAsync(URL);
DoSomethingWithData(stringData);
};
コードでは、Task
オブジェクトとの対話に煩わされることなく意図すること (データを非同期的にダウンロードする) が表されています。
CPUに依存する例:ゲーム計算を実行する
次の例では、モバイル ゲームがボタン イベントに応答して画面上の複数のエージェントに損害を与えます。 破損の計算を実行すると、コストがかかる場合があります。 UI スレッドで計算を実行すると、計算中に表示と UI の相互作用の問題が発生する可能性があります。
タスクを処理する最善の方法は、バックグラウンド スレッドを開始して、 Task.Run
メソッドで作業を完了することです。 この操作は、 await
式を使用して生成されます。 タスクが完了すると、操作が再開されます。 この方法では、バックグラウンドで作業が完了している間に UI をスムーズに実行できます。
static DamageResult CalculateDamageDone()
{
return new DamageResult()
{
// Code omitted:
//
// Does an expensive calculation and returns
// the result of that calculation.
};
}
s_calculateButton.Clicked += async (o, e) =>
{
// This line will yield control to the UI while CalculateDamageDone()
// performs its work. The UI thread is free to perform other work.
var damageResult = await Task.Run(() => CalculateDamageDone());
DisplayDamage(damageResult);
};
このコードは、イベント Clicked
ボタンの意図を明確に表しています。 バックグラウンド スレッドを手動で管理する必要はありません。また、非ブロッキングの方法でタスクを完了します。
CPU バインドと I/O バインドのシナリオを認識する
前の例では、I/O バインドおよび CPU バインド作業に対して async
修飾子と await
式を使用する方法を示します。 各シナリオの例では、操作がバインドされている場所に基づいてコードがどのように異なるかを示します。 実装を準備するには、操作が I/O バインドまたは CPU バインドのタイミングを識別する方法を理解する必要があります。 実装の選択は、コードのパフォーマンスに大きく影響し、コンストラクトが誤って使用される可能性があります。
コードを記述する前に、主に 2 つの質問に対処する必要があります。
質問 | シナリオ | 実装 |
---|---|---|
コードは、データベースからのデータなど、結果またはアクションを待機する必要がありますか? | I/O バウンド |
async メソッドawait 、修飾子とTask.Run 式を使用します。 タスク並列ライブラリは使用しないでください。 |
コードは高価な計算を実行する必要がありますか? | CPU 制約 |
async 修飾子とawait 式を使用しますが、Task.Run メソッドを使用して別のスレッドで作業を開始します。 この方法では、CPU の応答性に関する問題に対処します。 処理がコンカレンシーと並列処理に適している場合は、タスク並列ライブラリを使うことも考慮します。 |
常にコードの実行を測定します。 マルチスレッド時のコンテキスト切り替えのオーバーヘッドに比べて、CPU に依存する作業が十分にコストがかからないことに気づくかもしれません。 すべての選択肢にトレードオフがあります。 状況に合った適切なトレードオフを選択します。
その他の例を調べる
このセクションの例では、C# で非同期コードを記述する方法をいくつか示します。 これらは、発生する可能性のあるいくつかのシナリオに対応しています。
ネットワークからデータを抽出する
次のコードは、特定の URL から HTML をダウンロードし、文字列 ".NET" が HTML で発生した回数をカウントします。 このコードでは、ASP.NET を使用して、タスクを実行してカウントを返す Web API コントローラー メソッドを定義します。
注
運用コードで HTML の解析の実行を計画している場合は、正規表現を使用しないでください。 代わりに解析ライブラリを使用します。
[HttpGet, Route("DotNetCount")]
static public async Task<int> GetDotNetCount(string URL)
{
// Suspends GetDotNetCount() to allow the caller (the web server)
// to accept another request, rather than blocking on this one.
var html = await s_httpClient.GetStringAsync(URL);
return Regex.Matches(html, @"\.NET").Count;
}
ユニバーサル Windows アプリの同様のコードを記述し、ボタンを押した後にカウント タスクを実行できます。
private readonly HttpClient _httpClient = new HttpClient();
private async void OnSeeTheDotNetsButtonClick(object sender, RoutedEventArgs e)
{
// Capture the task handle here so we can await the background task later.
var getDotNetFoundationHtmlTask = _httpClient.GetStringAsync("https://dotnetfoundation.org");
// Any other work on the UI thread can be done here, such as enabling a Progress Bar.
// It's important to do the extra work here before the "await" call,
// so the user sees the progress bar before execution of this method is yielded.
NetworkProgressBar.IsEnabled = true;
NetworkProgressBar.Visibility = Visibility.Visible;
// The await operator suspends OnSeeTheDotNetsButtonClick(), returning control to its caller.
// This action is what allows the app to be responsive and not block the UI thread.
var html = await getDotNetFoundationHtmlTask;
int count = Regex.Matches(html, @"\.NET").Count;
DotNetCountLabel.Text = $"Number of .NETs on dotnetfoundation.org: {count}";
NetworkProgressBar.IsEnabled = false;
NetworkProgressBar.Visibility = Visibility.Collapsed;
}
複数タスクの完了を待機する
シナリオによっては、コードで複数のデータを同時に取得する必要があります。
Task
API には、複数のバックグラウンド ジョブで非ブロッキング待機を実行する非同期コードを記述できるメソッドが用意されています。
- Task.WhenAll メソッド
- Task.WhenAny メソッド
次の例は、一連の User
オブジェクトの userId
オブジェクト データを取得する方法を示しています。
private static async Task<User> GetUserAsync(int userId)
{
// Code omitted:
//
// Given a user Id {userId}, retrieves a User object corresponding
// to the entry in the database with {userId} as its Id.
return await Task.FromResult(new User() { id = userId });
}
private static async Task<IEnumerable<User>> GetUsersAsync(IEnumerable<int> userIds)
{
var getUserTasks = new List<Task<User>>();
foreach (int userId in userIds)
{
getUserTasks.Add(GetUserAsync(userId));
}
return await Task.WhenAll(getUserTasks);
}
LINQ を使用すると、このコードをより簡潔に記述できます。
private static async Task<User[]> GetUsersAsyncByLINQ(IEnumerable<int> userIds)
{
var getUserTasks = userIds.Select(id => GetUserAsync(id)).ToArray();
return await Task.WhenAll(getUserTasks);
}
LINQ を使用して記述するコードは少なくなりますが、LINQ と非同期コードを混在させる場合は注意が必要です。 LINQ は遅延実行が使用されます。
foreach
または .ToList()
メソッドの呼び出しを使用して生成されたシーケンスを強制的に反復処理しない限り、非同期呼び出しは、.ToArray()
ループ内で行われるようにすぐには行われません。 この例では、 Enumerable.ToArray メソッドを使用してクエリを熱心に実行し、結果を配列に格納します。 この方法では、 id => GetUserAsync(id)
ステートメントが強制的に実行され、タスクが開始されます。
非同期プログラミングに関する考慮事項を確認する
非同期プログラミングでは、予期しない動作を防ぐことができるいくつかの詳細に留意する必要があります。
async() メソッド本体内で await を使用する
async
修飾子を使用する場合は、メソッド本体に 1 つ以上のawait
式を含める必要があります。 コンパイラで await
式が見つからない場合、メソッドは生成に失敗します。 コンパイラは警告を生成しますが、コードは引き続きコンパイルされ、コンパイラはメソッドを実行します。 非同期メソッド用に C# コンパイラによって生成されたステート マシンでは何も実行されないため、プロセス全体が非常に非効率的です。
非同期メソッド名に "Async" サフィックスを追加する
.NET スタイルの規則では、すべての非同期メソッド名に "Async" サフィックスを追加します。 この方法は、同期メソッドと非同期メソッドをより簡単に区別するのに役立ちます。 コードによって明示的に呼び出されない特定のメソッド (イベント ハンドラーや Web コントローラー メソッドなど) は、必ずしもこのシナリオでは適用されません。 これらの項目はコードによって明示的に呼び出されないため、明示的な名前付けの使用はそれほど重要ではありません。
イベント ハンドラーからのみ 'async void' を返します
イベント ハンドラーは void
戻り値の型を宣言する必要があり、他のメソッドと同様に Task
オブジェクトと Task<T>
オブジェクトを使用または返すことはできません。 非同期イベント ハンドラーを記述するときは、ハンドラーのasync
戻りメソッドでvoid
修飾子を使用する必要があります。
async void
戻りメソッドの他の実装は TAP モデルに従っず、課題を提示する可能性があります。
-
async void
メソッドでスローされた例外を、そのメソッドの外部でキャッチすることはできません -
async void
メソッドのテストが困難である -
async void
メソッドは、呼び出し元が非同期であると想定していない場合に悪影響を及ぼす可能性があります
LINQ の非同期ラムダには注意が必要です
LINQ 式で非同期ラムダを実装するときは注意が必要です。 LINQ のラムダ式では遅延実行が使用されます。つまり、予期しないタイミングでコードが実行される可能性があります。 このシナリオにブロック タスクを導入すると、コードが正しく記述されていない場合、デッドロックが発生する可能性があります。 さらに、非同期コードを入れ子にすると、コードの実行について推論するのが困難になる場合もあります。 Async と LINQ は強力ですが、これらの手法はできるだけ慎重かつ明確に組み合わせて使用する必要があります。
非ブロッキング方式でタスクを待機する
プログラムでタスクの結果が必要な場合は、 await
式を非ブロッキング方式で実装するコードを記述します。
Task
項目が完了するまで同期的に待機する手段として現在のスレッドをブロックすると、デッドロックが発生し、コンテキスト スレッドがブロックされる可能性があります。 このプログラミング手法では、より複雑なエラー処理が必要になる場合があります。 次の表は、タスクからのアクセスが非ブロッキングな方法でどのように結果を得るかについてのガイダンスを示しています。
タスクのシナリオ | 現在のコード | 'await' に置き換えます |
---|---|---|
バックグラウンド タスクの結果を取得する |
Task.Wait または Task.Result |
await |
タスクが完了したら続行する | Task.WaitAny |
await Task.WhenAny |
すべてのタスクが完了したら続行する | Task.WaitAll |
await Task.WhenAll |
しばらくしてから続行する | Thread.Sleep |
await Task.Delay |
ValueTask 型の使用を検討する
非同期メソッドが Task
オブジェクトを返すと、特定のパスでパフォーマンスのボトルネックが発生する可能性があります。
Task
は参照型であるため、Task
オブジェクトはヒープから割り当てられます。
async
修飾子を使用して宣言されたメソッドがキャッシュされた結果を返すか、同期的に完了した場合、追加の割り当てによって、コードのパフォーマンスクリティカルセクションで大幅な時間コストが発生する可能性があります。 このシナリオは、厳密なループで割り当てが発生すると、コストがかかる場合があります。 詳しくは、「一般化された async の戻り値の型」をご覧ください。
ConfigureAwait(false) を設定するタイミングを理解する
開発者は、多くの場合、 Task.ConfigureAwait(Boolean) ブール値を使用するタイミングについて問い合わせています。 この API を使用すると、 Task
インスタンスは、任意の await
式を実装するステート マシンのコンテキストを構成できます。 ブール値が正しく設定されていない場合、パフォーマンスが低下したり、デッドロックが発生したりする可能性があります。 詳細については、「 ConfigureAwait FAQ」を参照してください。
ステートフルでないコードを記述する
グローバル オブジェクトの状態または特定のメソッドの実行に依存するコードを記述しないでください。 代わりに、メソッドの戻り値のみに依存するようにします。 ステートフルではないコードを記述する利点は多数あります。
- コードを理解しやすい
- 簡単にコードをテストする
- 非同期コードと同期コードを簡単に組み合わせることができます
- コード内の競合状態を回避できる
- 戻り値に依存する非同期コードを簡単に調整
- (ボーナス)コード内の依存関係の挿入に対して適切に機能する
推奨される目標は、完全またはほぼ完全な参照の透過性をコードで実現することです。 このアプローチにより、予測可能、テスト可能、保守可能なコードベースが得られます。
完全な例を確認する
次のコードは、 Program.cs サンプル ファイルで使用できる完全な例を表しています。
using System.Text.RegularExpressions;
using System.Windows;
using Microsoft.AspNetCore.Mvc;
class Button
{
public Func<object, object, Task>? Clicked
{
get;
internal set;
}
}
class DamageResult
{
public int Damage
{
get { return 0; }
}
}
class User
{
public bool isEnabled
{
get;
set;
}
public int id
{
get;
set;
}
}
public class Program
{
private static readonly Button s_downloadButton = new();
private static readonly Button s_calculateButton = new();
private static readonly HttpClient s_httpClient = new();
private static readonly IEnumerable<string> s_urlList = new string[]
{
"https://learn.microsoft.com",
"https://learn.microsoft.com/aspnet/core",
"https://learn.microsoft.com/azure",
"https://learn.microsoft.com/azure/devops",
"https://learn.microsoft.com/dotnet",
"https://learn.microsoft.com/dotnet/desktop/wpf/get-started/create-app-visual-studio",
"https://learn.microsoft.com/education",
"https://learn.microsoft.com/shows/net-core-101/what-is-net",
"https://learn.microsoft.com/enterprise-mobility-security",
"https://learn.microsoft.com/gaming",
"https://learn.microsoft.com/graph",
"https://learn.microsoft.com/microsoft-365",
"https://learn.microsoft.com/office",
"https://learn.microsoft.com/powershell",
"https://learn.microsoft.com/sql",
"https://learn.microsoft.com/surface",
"https://dotnetfoundation.org",
"https://learn.microsoft.com/visualstudio",
"https://learn.microsoft.com/windows",
"https://learn.microsoft.com/maui"
};
private static void Calculate()
{
// <PerformGameCalculation>
static DamageResult CalculateDamageDone()
{
return new DamageResult()
{
// Code omitted:
//
// Does an expensive calculation and returns
// the result of that calculation.
};
}
s_calculateButton.Clicked += async (o, e) =>
{
// This line will yield control to the UI while CalculateDamageDone()
// performs its work. The UI thread is free to perform other work.
var damageResult = await Task.Run(() => CalculateDamageDone());
DisplayDamage(damageResult);
};
// </PerformGameCalculation>
}
private static void DisplayDamage(DamageResult damage)
{
Console.WriteLine(damage.Damage);
}
private static void Download(string URL)
{
// <UnblockingDownload>
s_downloadButton.Clicked += async (o, e) =>
{
// This line will yield control to the UI as the request
// from the web service is happening.
//
// The UI thread is now free to perform other work.
var stringData = await s_httpClient.GetStringAsync(URL);
DoSomethingWithData(stringData);
};
// </UnblockingDownload>
}
private static void DoSomethingWithData(object stringData)
{
Console.WriteLine($"Displaying data: {stringData}");
}
// <GetUsersForDataset>
private static async Task<User> GetUserAsync(int userId)
{
// Code omitted:
//
// Given a user Id {userId}, retrieves a User object corresponding
// to the entry in the database with {userId} as its Id.
return await Task.FromResult(new User() { id = userId });
}
private static async Task<IEnumerable<User>> GetUsersAsync(IEnumerable<int> userIds)
{
var getUserTasks = new List<Task<User>>();
foreach (int userId in userIds)
{
getUserTasks.Add(GetUserAsync(userId));
}
return await Task.WhenAll(getUserTasks);
}
// </GetUsersForDataset>
// <GetUsersForDatasetByLINQ>
private static async Task<User[]> GetUsersAsyncByLINQ(IEnumerable<int> userIds)
{
var getUserTasks = userIds.Select(id => GetUserAsync(id)).ToArray();
return await Task.WhenAll(getUserTasks);
}
// </GetUsersForDatasetByLINQ>
// <ExtractDataFromNetwork>
[HttpGet, Route("DotNetCount")]
static public async Task<int> GetDotNetCount(string URL)
{
// Suspends GetDotNetCount() to allow the caller (the web server)
// to accept another request, rather than blocking on this one.
var html = await s_httpClient.GetStringAsync(URL);
return Regex.Matches(html, @"\.NET").Count;
}
// </ExtractDataFromNetwork>
static async Task Main()
{
Console.WriteLine("Application started.");
Console.WriteLine("Counting '.NET' phrase in websites...");
int total = 0;
foreach (string url in s_urlList)
{
var result = await GetDotNetCount(url);
Console.WriteLine($"{url}: {result}");
total += result;
}
Console.WriteLine("Total: " + total);
Console.WriteLine("Retrieving User objects with list of IDs...");
IEnumerable<int> ids = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
var users = await GetUsersAsync(ids);
foreach (User? user in users)
{
Console.WriteLine($"{user.id}: isEnabled={user.isEnabled}");
}
Console.WriteLine("Application ending.");
}
}
// Example output:
//
// Application started.
// Counting '.NET' phrase in websites...
// https://learn.microsoft.com: 0
// https://learn.microsoft.com/aspnet/core: 57
// https://learn.microsoft.com/azure: 1
// https://learn.microsoft.com/azure/devops: 2
// https://learn.microsoft.com/dotnet: 83
// https://learn.microsoft.com/dotnet/desktop/wpf/get-started/create-app-visual-studio: 31
// https://learn.microsoft.com/education: 0
// https://learn.microsoft.com/shows/net-core-101/what-is-net: 42
// https://learn.microsoft.com/enterprise-mobility-security: 0
// https://learn.microsoft.com/gaming: 0
// https://learn.microsoft.com/graph: 0
// https://learn.microsoft.com/microsoft-365: 0
// https://learn.microsoft.com/office: 0
// https://learn.microsoft.com/powershell: 0
// https://learn.microsoft.com/sql: 0
// https://learn.microsoft.com/surface: 0
// https://dotnetfoundation.org: 16
// https://learn.microsoft.com/visualstudio: 0
// https://learn.microsoft.com/windows: 0
// https://learn.microsoft.com/maui: 6
// Total: 238
// Retrieving User objects with list of IDs...
// 1: isEnabled= False
// 2: isEnabled= False
// 3: isEnabled= False
// 4: isEnabled= False
// 5: isEnabled= False
// 6: isEnabled= False
// 7: isEnabled= False
// 8: isEnabled= False
// 9: isEnabled= False
// 0: isEnabled= False
// Application ending.
関連リンク
.NET