前回は、Task、Parallel、PLINQ を使った並列処理を見ました。
今回は、現代の C# で非常によく使う async/await パターンを整理します。
async/await は、処理を別スレッドで実行するためだけの機能ではありません。
主な目的は、I/O 待ちのような「時間はかかるが CPU を使い続けない処理」を、スレッドをブロックせずに待てるようにすることです。
async と await の基本
async は、そのメソッドの中で await を使えるようにする修飾子です。
await は、Task などの非同期処理の完了を待ちます。
using System;
using System.Net.Http;
using System.Threading.Tasks;
using HttpClient client = new HttpClient();
string html = await client.GetStringAsync("https://example.com");
Console.WriteLine(html.Length);
await している間、現在のスレッドを占有して待ち続けるわけではありません。
I/O が完了したあと、続きの処理が再開されます。
非同期メソッドを書く
非同期メソッドは、通常 Task または Task<T> を返します。
using System.Net.Http;
using System.Threading.Tasks;
static async Task<int> GetContentLengthAsync(string url)
{
using HttpClient client = new HttpClient();
string content = await client.GetStringAsync(url);
return content.Length;
}
int length = await GetContentLengthAsync("https://example.com");
Console.WriteLine(length);
戻り値がある場合は Task<T>、戻り値がない場合は Task を返します。
static async Task DelayAndPrintAsync()
{
await Task.Delay(1000);
Console.WriteLine("完了しました。");
}
async void は基本的に避ける
戻り値がない非同期メソッドでも、基本的には Task を返します。
static async Task SaveAsync()
{
await Task.Delay(500);
Console.WriteLine("保存しました。");
}
async void は、主にイベントハンドラー用です。
private async void SaveButton_Click(object sender, EventArgs e)
{
await SaveAsync();
}
async void は呼び出し側が完了や例外を追跡しづらいため、通常のメソッドでは避けます。
SynchronizationContext と async/await
await の後、どのスレッドで処理が再開されるかは実行環境によって変わります。
WPF や Windows Forms のような UI アプリには、UI スレッドへ戻るための SynchronizationContext があります。
ASP.NET やコンソールアプリでは、環境によって挙動が異なります。
UI アプリでは、次のようなコードが自然に書けます。
private async void LoadButton_Click(object sender, EventArgs e)
{
StatusLabel.Text = "読み込み中...";
string text = await LoadTextAsync();
StatusLabel.Text = text;
}
await の後に UI スレッドへ戻るため、UI コントロールを更新できます。
ConfigureAwait の役割
ライブラリコードでは、ConfigureAwait(false) を使うことがあります。
static async Task<string> LoadTextAsync(HttpClient client)
{
string text = await client
.GetStringAsync("https://example.com")
.ConfigureAwait(false);
return text;
}
ConfigureAwait(false) は、「await 後に元のコンテキストへ戻らなくてもよい」と伝えます。
UI 更新が必要なアプリケーションコードでは、むやみに付けると UI スレッドへ戻らず困ることがあります。
一方、UI に依存しないライブラリコードでは、不要なコンテキスト復帰を避けられるため有用です。
非同期メソッドの命名規約
非同期メソッドには、末尾に Async を付けるのが一般的です。
Task SaveAsync()
Task<string> LoadTextAsync()
Task<int> CountItemsAsync()
Async サフィックスがあると、呼び出し側は await すべきメソッドだと判断しやすくなります。
例外として、イベントハンドラーやインターフェイス制約など、命名を自由に変えられない場合もあります。
複数の await を使う
非同期メソッド内では、複数の await を順番に書けます。
using System.Net.Http;
using System.Threading.Tasks;
static async Task PrintTwoPagesAsync()
{
using HttpClient client = new HttpClient();
string first = await client.GetStringAsync("https://example.com");
string second = await client.GetStringAsync("https://example.org");
Console.WriteLine(first.Length);
Console.WriteLine(second.Length);
}
このコードでは、1 つ目が終わってから 2 つ目を開始します。
独立した処理を同時に始めたい場合は、先に Task を作ってから await Task.WhenAll() します。
using System.Net.Http;
using System.Threading.Tasks;
static async Task PrintTwoPagesInParallelAsync()
{
using HttpClient client = new HttpClient();
Task<string> firstTask = client.GetStringAsync("https://example.com");
Task<string> secondTask = client.GetStringAsync("https://example.org");
string[] results = await Task.WhenAll(firstTask, secondTask);
Console.WriteLine(results[0].Length);
Console.WriteLine(results[1].Length);
}
await は「待つ」構文なので、どこで Task を開始するかが重要です。
同期メソッドから非同期メソッドを呼ぶ
非同期メソッドは、できるだけ呼び出し元まで async を伝播させるのが基本です。
static async Task Main()
{
await SaveAsync();
}
どうしても同期メソッドから呼ぶ場合、Result や Wait() を使うとブロックします。
SaveAsync().GetAwaiter().GetResult();
これは例外の包み方が比較的扱いやすいものの、ブロックであることには変わりません。
UI アプリや ASP.NET ではデッドロックや応答性低下の原因になるため、可能な限り async all the way を意識します。
catch と finally で await する
C# では、catch や finally の中でも await を使えます。
static async Task SaveWithCleanupAsync()
{
try
{
await SaveAsync();
}
catch (Exception ex)
{
await LogAsync(ex);
throw;
}
finally
{
await CleanupAsync();
}
}
エラーログ送信や一時ファイル削除が非同期 API の場合でも、自然に後始末を書けます。
ValueTask を返す非同期メソッド
Task<T> の代わりに ValueTask<T> を返すメソッドもあります。
using System.Threading.Tasks;
static ValueTask<int> GetCachedValueAsync(bool cached)
{
if (cached)
{
return ValueTask.FromResult(42);
}
return new ValueTask<int>(LoadValueAsync());
}
static async Task<int> LoadValueAsync()
{
await Task.Delay(500);
return 42;
}
ValueTask<T> は、同期的に結果を返せることが多い高性能な API で使われます。
通常のアプリケーションコードでは、まず Task / Task<T> を使えば十分です。
ローカル関数と async/await
ローカル関数にも async を付けられます。
using System;
using System.Threading.Tasks;
static async Task RunAsync()
{
await PrintAfterDelayAsync("A");
await PrintAfterDelayAsync("B");
static async Task PrintAfterDelayAsync(string message)
{
await Task.Delay(500);
Console.WriteLine(message);
}
}
メソッド内だけで使う小さな非同期処理を切り出すと、処理の流れが読みやすくなります。
async/await のキャンセル
非同期処理でも、キャンセルには CancellationToken を使います。
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
static async Task<string> DownloadAsync(string url, CancellationToken token)
{
using HttpClient client = new HttpClient();
return await client.GetStringAsync(url, token);
}
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
try
{
string text = await DownloadAsync("https://example.com", cts.Token);
Console.WriteLine(text.Length);
}
catch (OperationCanceledException)
{
Console.WriteLine("キャンセルされました。");
}
キャンセル対応 API には、できるだけ CancellationToken を渡します。
独自の非同期メソッドでも、長い処理では token を受け取る設計にしておくと呼び出し側が制御しやすくなります。
非同期ストリーム
IAsyncEnumerable<T> を使うと、非同期に値を 1 つずつ返せます。
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
static async IAsyncEnumerable<int> GenerateNumbersAsync()
{
for (int i = 1; i <= 5; i++)
{
await Task.Delay(300);
yield return i;
}
}
await foreach (int number in GenerateNumbersAsync())
{
Console.WriteLine(number);
}
大量データを一括で返すのではなく、準備できたものから順に流したい場合に便利です。
API のページング、ログの読み取り、ストリーミング処理などと相性がよいです。
Parallel.ForEachAsync
.NET 6 以降では、Parallel.ForEachAsync を使って非同期処理を並列実行できます。
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
string[] urls =
{
"https://example.com",
"https://example.org",
"https://example.net"
};
using HttpClient client = new HttpClient();
await Parallel.ForEachAsync(
urls,
new ParallelOptions { MaxDegreeOfParallelism = 2 },
async (url, token) =>
{
string text = await client.GetStringAsync(url, token);
Console.WriteLine($"{url}: {text.Length}");
});
Parallel.ForEach は同期処理の並列化に向いています。
Parallel.ForEachAsync は、HTTP リクエストのような非同期処理を複数同時に進めたい場合に向いています。
まとめ
async/await は、非同期処理を読みやすく書くための C# の中心機能です。
Task や Task<T> は非同期処理の結果を表し、await は完了を自然に待つための構文です。
UI アプリでは SynchronizationContext、ライブラリコードでは ConfigureAwait(false)、長時間処理では CancellationToken を意識すると、実務で扱いやすいコードになります。
また、非同期ストリームや Parallel.ForEachAsync を使うことで、現代的な I/O 中心の処理も自然に表現できます。