C# Taskとは?async/await・Task.Run・WhenAllの使い方と非同期処理の注意点を初心者向けに解説
はじめに
C#のTaskは、時間のかかる処理を効率よく扱うための中心的な仕組みです。Web APIへのアクセス、ファイルの読み書き、データベース処理、重い計算などで利用されます。
ただし、Task、async、await、Task.Run、Task.WhenAllは、それぞれ役割が異なります。使い分けを理解しないまま利用すると、画面のフリーズ、デッドロック、例外の見落とし、スレッドプールの枯渇などにつながることがあります。
この記事では、C#のTaskの基本から、async・await、Task.Run、Task.WhenAll、例外処理、キャンセル処理までを、初心者向けにコード例を交えて解説します。
1. C#のTaskとは
1-1. Taskは「非同期処理の進行状況や完了」を表すクラス
C#のTaskは、実行中または将来完了する処理を表すクラスです。処理そのものだけでなく、処理が完了したか、失敗したか、キャンセルされたかといった状態も保持します。
たとえば、次のコードではTask.Delayが、1秒後に完了するTaskを返します。
C#Task delayTask = Task.Delay(1000);
await delayTask;
Console.WriteLine("1秒経過しました");
Taskを受け取った呼び出し元は、awaitを使って処理の完了を待てます。Microsoftのドキュメントでも、C#の非同期処理ではTaskとTask<TResult>が処理の進行や結果を表す基本的な型として説明されています。 Microsoft Learn+1
1-2. TaskとTask<TResult>の違い
Taskは、戻り値を持たない非同期処理を表します。
C#static async Task SaveAsync()
{
await Task.Delay(500);
Console.WriteLine("保存しました");
}
一方、Task<TResult>は、処理完了後にTResult型の値を返す非同期処理を表します。
C#static async Task<int> GetNumberAsync()
{
await Task.Delay(500);
return 100;
}
呼び出し側では、awaitの結果として値を受け取れます。
C#int number = await GetNumberAsync();
Console.WriteLine(number);
戻り値が不要ならTask、戻り値が必要ならTask<TResult>を選ぶのが基本です。 Microsoft Learn
1-3. Taskが持つ主な状態
Taskの状態は、Statusプロパティから確認できます。代表的な状態は次のとおりです。
Created:作成されたが、まだ開始されていないWaitingForActivation:.NET内部で開始を待っているWaitingToRun:実行予定として登録されているRunning:実行中RanToCompletion:正常に完了したCanceled:キャンセルされたFaulted:例外によって失敗した
C#Task task = Task.Delay(1000);
Console.WriteLine(task.Status);
await task;
Console.WriteLine(task.Status);
状態は実行中に変化するため、Statusを取得した直後に別の状態へ移っている可能性があります。通常は状態を監視するより、awaitと例外処理を使って完了を扱います。 Microsoft Learn+1
1-4. Taskとスレッドは同じものではない
Taskとスレッドは同じものではありません。
スレッドは、CPU上でコードを実行するための実行単位です。一方のTaskは、非同期処理の進行や完了を表す抽象的なオブジェクトです。
たとえば、ネットワーク通信の完了を待っている間、そのTaskのためにスレッドが待機し続けるとは限りません。I/O処理が完了すると、後続処理がスレッドプールなどで再開されます。
また、1つのTaskが最初から最後まで同じスレッドで実行される保証もありません。
C#static async Task ShowThreadAsync()
{
Console.WriteLine($"開始: {Environment.CurrentManagedThreadId}");
await Task.Delay(1000);
Console.WriteLine($"再開: {Environment.CurrentManagedThreadId}");
}
実行環境によっては、awaitの前後でスレッドIDが変わることがあります。
1-5. 同期処理・非同期処理・並列処理の違い
同期処理は、現在の処理が完了するまで次の処理へ進まない実行方式です。
C#DoWork1();
DoWork2();
非同期処理は、時間のかかる処理の完了を待っている間に、スレッドを別の仕事へ利用できるようにする仕組みです。
C#await DoWork1Async();
await DoWork2Async();
並列処理は、複数の処理を複数のCPUコアなどで同時に実行することです。
C#Task task1 = Task.Run(() => HeavyWork1());
Task task2 = Task.Run(() => HeavyWork2());
await Task.WhenAll(task1, task2);
非同期処理は必ずしも並列処理ではありません。特にI/O待ちの非同期処理では、待機中にCPUを使わないことが重要であり、複数スレッドで計算しているとは限りません。
1-6. C#でTaskを使うメリット
Taskを使う主なメリットは、アプリケーションの応答性とリソース効率を高められることです。
デスクトップアプリでは、通信やファイル処理の間もUIを操作できるようになります。Webアプリでは、I/O待ちの間にスレッドを解放し、別のリクエストを処理できます。
さらに、次のような処理も記述しやすくなります。
複数処理の完了をまとめて待つ
最初に完了した処理を取得する
例外を呼び出し元へ伝える
処理をキャンセルする
タイムアウトを設定する
2. C#でTaskを使う基本的な方法
2-1. Taskを返す非同期メソッドの書き方
戻り値のない非同期メソッドは、async Taskとして定義します。
C#static async Task PrintMessageAsync()
{
await Task.Delay(1000);
Console.WriteLine("処理が完了しました");
}
呼び出し側ではawaitを付けます。
C#await PrintMessageAsync();
awaitを利用する呼び出し元のメソッドにも、通常はasyncを付けてTaskを返します。
2-2. Task<TResult>で処理結果を返す方法
処理結果を返す場合はTask<TResult>を使用します。
C#static async Task<string> GetMessageAsync()
{
await Task.Delay(500);
return "取得成功";
}
呼び出し側では次のように結果を受け取ります。
C#string message = await GetMessageAsync();
Console.WriteLine(message);
returnで返した値が、await式の結果になります。
2-3. asyncメソッドの命名に「Async」を付ける理由
非同期メソッドには、一般的にAsyncという接尾辞を付けます。
C#GetDataAsync();
SaveFileAsync();
SendMessageAsync();
Asyncを付けることで、呼び出し側はそのメソッドがTaskを返し、awaitして利用するものだと判断できます。
コンパイラ上の必須ルールではありませんが、同期版との区別やコードの可読性を高めるために重要な命名規則です。
2-4. Task.CompletedTaskで完了済みTaskを返す方法
非同期インターフェースを実装しているものの、実際には待機処理がない場合は、Task.CompletedTaskを返せます。
C#static Task WriteLogAsync(string message)
{
Console.WriteLine(message);
return Task.CompletedTask;
}
この方法なら、不要なasyncステートマシンを生成せずに完了済みのTaskを返せます。
2-5. Task.FromResultで値を持つ完了済みTaskを作る方法
値を持つ完了済みのTask<TResult>は、Task.FromResultで作成できます。
C#static Task<int> GetDefaultValueAsync()
{
return Task.FromResult(10);
}
キャッシュに値がある場合など、非同期メソッドと同じ戻り値の型を保ちながら、即座に値を返したい場面で利用できます。
C#static Task<string> GetNameAsync(int id)
{
if (id == 1)
{
return Task.FromResult("田中");
}
return LoadNameFromDatabaseAsync(id);
}
2-6. asyncを付けずにTaskを直接返すケース
メソッド内で単一の非同期処理を呼び出し、その結果を加工せずに返すだけなら、asyncとawaitを省略できる場合があります。
C#static Task<string> DownloadAsync(HttpClient client, string url)
{
return client.GetStringAsync(url);
}
次のように書くこともできますが、単純な中継だけなら冗長です。
C#static async Task<string> DownloadAsync(HttpClient client, string url)
{
return await client.GetStringAsync(url);
}
ただし、メソッド内でtry-catchしたい場合、後処理を追加したい場合、複数のawaitを使う場合は、asyncを付ける方が自然です。
3. asyncとawaitの仕組みと使い方
3-1. asyncキーワードの役割
asyncは、そのメソッド内でawaitを利用できるようにするキーワードです。
C#static async Task ExecuteAsync()
{
await Task.Delay(1000);
}
asyncを付けるだけで、自動的に別スレッドで実行されるわけではありません。asyncメソッドは、完了していないTaskをawaitする地点までは、通常の同期コードと同じように実行されます。
3-2. awaitキーワードの役割
awaitは、対象のTaskが完了するまで、そのメソッドの残りの処理を一時停止します。
C#Console.WriteLine("開始");
await Task.Delay(1000);
Console.WriteLine("完了");
awaitの重要な点は、単に「処理が終わるまで待つ」だけではなく、待機中に現在のスレッドをブロックしないことです。
Taskが完了すると、awaitより後ろの処理が継続処理として実行されます。 Microsoft Learn+1
3-3. await中に呼び出し元のスレッドがブロックされない仕組み
完了していないTaskをawaitすると、メソッドは呼び出し元へ制御を返します。
C#static async Task WaitAsync()
{
Console.WriteLine("待機開始");
await Task.Delay(3000);
Console.WriteLine("待機終了");
}
Task.Delayの間、スレッドが3秒間停止しているわけではありません。タイマーの完了通知を受けた後、継続処理が実行可能なスレッド上で再開されます。
この仕組みにより、UIスレッドは画面操作を処理でき、Webサーバーのスレッドは別のリクエストを処理できます。
3-4. asyncだけ付けてawaitしない場合の動作
asyncを付けたメソッド内にawaitがない場合、処理は同期的に実行されます。
C#static async Task PrintAsync()
{
Console.WriteLine("同期的に実行されます");
}
このコードでは、コンパイラから「この非同期メソッドにはawaitがない」という警告が出ます。
待機処理がないなら、次のように書く方が適切です。
C#static Task PrintAsync()
{
Console.WriteLine("同期的に実行されます");
return Task.CompletedTask;
}
3-5. awaitしたTaskから戻り値を受け取る方法
Task<TResult>をawaitすると、TResult型の値を取得できます。
C#static async Task<int> CalculateAsync()
{
await Task.Delay(500);
return 20 + 30;
}
int result = await CalculateAsync();
Console.WriteLine(result);
CalculateAsync()の戻り値はTask<int>ですが、await CalculateAsync()の結果はintです。
3-6. 複数のawaitを順番に実行する方法
awaitを順番に書くと、前の処理が完了してから次の処理が開始されます。
C#string user = await GetUserAsync();
string orders = await GetOrdersAsync();
string points = await GetPointsAsync();
それぞれの処理が前の結果に依存している場合は、この書き方が適切です。
C#User user = await GetUserAsync();
Order[] orders = await GetOrdersAsync(user.Id);
Receipt receipt = await CreateReceiptAsync(orders);
一方、処理同士が独立しているなら、先にすべて開始してTask.WhenAllで待つことで、全体の待ち時間を短縮できる可能性があります。
3-7. Mainメソッドをasyncにする方法
現在のC#では、Mainメソッドをasync Taskまたはasync Task<int>として定義できます。
C#class Program
{
static async Task Main()
{
Console.WriteLine("開始");
await Task.Delay(1000);
Console.WriteLine("終了");
}
}
終了コードを返す場合は、Task<int>を使用します。
C#static async Task<int> Main()
{
await Task.Delay(1000);
return 0;
}
3-8. UIアプリで画面を固めずに処理する例
WPFやWindows Formsなどでは、UIスレッド上で時間のかかる同期処理を実行すると、画面が操作不能になります。
非同期APIがある場合は、イベントハンドラー内でawaitします。
C#private async void DownloadButton_Click(
object sender,
EventArgs e)
{
downloadButton.Enabled = false;
try
{
using HttpClient client = new();
string text = await client.GetStringAsync(
"https://example.com");
resultTextBox.Text = text;
}
catch (HttpRequestException ex)
{
MessageBox.Show(ex.Message);
}
finally
{
downloadButton.Enabled = true;
}
}
await中はUIスレッドをブロックしないため、ウィンドウの再描画などを継続できます。
4. Task.Runの使い方
4-1. Task.Runとは
Task.Runは、指定した処理をスレッドプールへ登録し、その処理を表すTaskを返すメソッドです。
C#Task task = Task.Run(() =>
{
Console.WriteLine("別のスレッドプールスレッドで実行");
});
await task;
主に、CPU負荷が高い同期処理を、UIスレッドなどから切り離して実行する場合に使います。 Microsoft Learn+1
4-2. Task.Runで同期処理を別のスレッドプール上で実行する方法
同期処理をTask.Runへ渡す基本形は次のとおりです。
C#await Task.Run(() =>
{
DoSynchronousWork();
});
既存の同期メソッドも渡せます。
C#await Task.Run(DoSynchronousWork);
ただし、同期的なI/O処理をすべてTask.Runで包めばよいわけではありません。利用できるなら、最初から非同期に設計されたAPIを優先します。
4-3. Task.Runから戻り値を受け取る方法
ラムダ式から値を返すと、Task<TResult>が返されます。
C#Task<int> task = Task.Run(() =>
{
return 10 * 20;
});
int result = await task;
Console.WriteLine(result);
型推論を使って簡潔に書くこともできます。
C#int result = await Task.Run(() => 10 * 20);
4-4. CPU負荷の高い処理でTask.Runを使う例
大量の計算をUIスレッド上で実行すると、画面が固まります。その場合はTask.Runを検討できます。
C#static long CalculateSum(int max)
{
long sum = 0;
for (int i = 1; i <= max; i++)
{
sum += i;
}
return sum;
}
long result = await Task.Run(
() => CalculateSum(100_000_000));
Console.WriteLine(result);
Task.Runによって処理そのものが軽くなるわけではありませんが、呼び出し元のスレッドを長時間占有することを避けられます。
4-5. I/O待ちの非同期処理にTask.Runが不要な理由
HTTP通信や非同期ファイル操作など、すでに非同期APIが用意されている処理をTask.Runで包む必要はありません。
次のコードは不要です。
C#string text = await Task.Run(
() => client.GetStringAsync(url));
単純に非同期APIをawaitします。
C#string text = await client.GetStringAsync(url);
I/O待ちの非同期APIは、待機中にスレッドを占有しないよう設計されています。Task.Runで包むと、余分なスケジューリングが発生します。
4-6. Task.Runとasync・awaitを組み合わせる方法
CPU負荷の高い処理を呼び出す側では、Task.Runをawaitできます。
C#static async Task<long> CalculateAsync(int max)
{
return await Task.Run(() =>
{
long sum = 0;
for (int i = 1; i <= max; i++)
{
sum += i;
}
return sum;
});
}
ただし、ライブラリ側が安易に同期処理をTask.Runで包むのではなく、CPU処理を別スレッドへ移すかどうかは、アプリケーション側が判断する設計も有効です。
4-7. Task.Run内でasyncラムダ式を使う場合の注意点
Task.Runにはasyncラムダ式も渡せます。
C#await Task.Run(async () =>
{
await Task.Delay(1000);
Console.WriteLine("完了");
});
Task.Run(Func<Task>)は、内部の非同期処理が完了するまで追跡できるTaskを返します。
ただし、次のようなasync void相当のデリゲートは避けます。
C#Action action = async () =>
{
await Task.Delay(1000);
throw new InvalidOperationException();
};
Actionは戻り値がvoidであるため、呼び出し元が処理の完了や例外を追跡しにくくなります。非同期ラムダ式は、原則としてFunc<Task>として扱います。
4-8. ASP.NET CoreでTask.Runを多用すべきでない理由
ASP.NET Coreでは、リクエスト処理自体が通常スレッドプール上で実行されます。そのため、同期処理をTask.Runへ移してすぐawaitしても、スレッドプール上の仕事を別のスレッドプールスレッドへ移すだけになりがちです。
特に同期I/OをTask.Runで包む方法は、アクセスが増えたときにスレッドプールを圧迫する可能性があります。データベースやHTTP通信では、可能な限り非同期APIを利用します。
長時間のバックグラウンド処理が必要な場合は、ホステッドサービス、キュー、外部のジョブ基盤などを検討します。 Microsoft Learn
5. Task.WhenAllで複数の非同期処理を同時に待つ方法
5-1. Task.WhenAllとは
Task.WhenAllは、渡されたすべてのTaskが完了したときに完了する、新しいTaskを作成します。
C#await Task.WhenAll(task1, task2, task3);
複数の独立した処理を並行して進め、すべての完了を待ちたい場合に使用します。 Microsoft Learn
5-2. 複数のTaskを並行して開始する基本コード
重要なのは、各処理を先に開始してからTask.WhenAllで待つことです。
C#Task<string> userTask = GetUserAsync();
Task<string> orderTask = GetOrdersAsync();
Task<string> newsTask = GetNewsAsync();
await Task.WhenAll(
userTask,
orderTask,
newsTask);
Console.WriteLine(await userTask);
Console.WriteLine(await orderTask);
Console.WriteLine(await newsTask);
各メソッドを呼び出した時点で処理が開始され、3つの処理が並行して進みます。
5-3. Task<TResult>の結果を配列で受け取る方法
同じ型のTask<TResult>をWhenAllへ渡すと、結果を配列として受け取れます。
C#Task<int> task1 = GetScoreAsync(1);
Task<int> task2 = GetScoreAsync(2);
Task<int> task3 = GetScoreAsync(3);
int[] scores = await Task.WhenAll(
task1,
task2,
task3);
foreach (int score in scores)
{
Console.WriteLine(score);
}
結果配列の順番は、処理が完了した順番ではなく、WhenAllへ渡したTaskの順番に対応します。
5-4. awaitを順番に書く場合とWhenAllを使う場合の違い
次のコードでは、1つ目が完了してから2つ目を開始します。
C#string result1 = await GetDataAsync(1);
string result2 = await GetDataAsync(2);
各処理に2秒かかる場合、合計で約4秒かかる可能性があります。
先に両方を開始すると、並行して待てます。
C#Task<string> task1 = GetDataAsync(1);
Task<string> task2 = GetDataAsync(2);
string[] results = await Task.WhenAll(
task1,
task2);
この場合、両方が約2秒で完了するなら、全体も約2秒で完了する可能性があります。
ただし、後の処理が前の結果に依存する場合は、順番にawaitする必要があります。
5-5. foreachとTask.WhenAllを組み合わせる方法
複数のIDに対して処理を行う場合は、Taskのリストを作成します。
C#var tasks = new List<Task<string>>();
foreach (int id in ids)
{
tasks.Add(GetDataAsync(id));
}
string[] results = await Task.WhenAll(tasks);
ループ内でそのままawaitすると逐次実行になります。
C#foreach (int id in ids)
{
string result = await GetDataAsync(id);
}
逐次実行が必要なのか、並行実行してよいのかを判断して使い分けます。
5-6. LINQで複数のTaskを生成してWhenAllで待つ方法
LINQを使うと、処理対象からTaskを生成できます。
C#Task<string>[] tasks = ids
.Select(id => GetDataAsync(id))
.ToArray();
string[] results = await Task.WhenAll(tasks);
さらに簡潔に書くこともできます。
C#string[] results = await Task.WhenAll(
ids.Select(GetDataAsync));
ただし、一度に大量の処理を開始すると、接続先のサーバーやデータベースへ過剰な負荷をかける可能性があります。
5-7. WhenAllで一部のTaskが失敗した場合の動作
Task.WhenAllへ渡した処理の一部が失敗しても、通常はほかの処理も継続され、すべてが終了した時点でWhenAllのTaskが失敗状態になります。
C#try
{
await Task.WhenAll(task1, task2, task3);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
複数の処理が失敗した場合、awaitでは代表的な例外が再スローされます。すべての例外を確認したい場合は、WhenAllが返すTaskのExceptionを調べます。
C#Task allTask = Task.WhenAll(
task1,
task2,
task3);
try
{
await allTask;
}
catch
{
if (allTask.Exception is not null)
{
foreach (Exception ex
in allTask.Exception.InnerExceptions)
{
Console.WriteLine(ex.Message);
}
}
}
5-8. 同時実行数が多すぎる場合の負荷対策
数千件のHTTPリクエストなどを一度に開始すると、メモリ、ソケット、接続先サービスへ大きな負荷をかけます。
SemaphoreSlimを使うと、同時実行数を制限できます。
C#static async Task<string[]> DownloadAllAsync(
IEnumerable<string> urls,
HttpClient client,
CancellationToken cancellationToken)
{
using SemaphoreSlim semaphore = new(5);
IEnumerable<Task<string>> tasks = urls.Select(
async url =>
{
await semaphore.WaitAsync(cancellationToken);
try
{
return await client.GetStringAsync(
url,
cancellationToken);
}
finally
{
semaphore.Release();
}
});
return await Task.WhenAll(tasks);
}
この例では、同時に実行するダウンロードを最大5件に制限しています。
6. Task.WhenAnyで最初に完了した処理を扱う方法
6-1. Task.WhenAnyとは
Task.WhenAnyは、渡されたTaskのうち、どれか1つが完了した時点で完了するTaskを返します。
C#Task completedTask = await Task.WhenAny(
task1,
task2,
task3);
戻り値は、最初に完了したTaskです。最初に完了した処理が失敗またはキャンセルされている可能性もあるため、そのTask自体をさらにawaitして結果を確認します。 Microsoft Learn
6-2. 最初に完了したTaskの結果を取得する方法
Task<TResult>の場合は、WhenAnyの結果をもう一度awaitします。
C#Task<string> task1 = GetDataAsync(1);
Task<string> task2 = GetDataAsync(2);
Task<string> completedTask =
await Task.WhenAny(task1, task2);
string result = await completedTask;
Console.WriteLine(result);
最初のawaitは「どのTaskが完了したか」を取得し、2回目のawaitはそのTaskの結果または例外を取得します。
6-3. WhenAnyを繰り返して完了順に処理する方法
複数の処理結果を完了順に扱う場合は、完了したTaskをリストから削除します。
C#var tasks = ids
.Select(GetDataAsync)
.ToList();
while (tasks.Count > 0)
{
Task<string> completedTask =
await Task.WhenAny(tasks);
tasks.Remove(completedTask);
try
{
string result = await completedTask;
Console.WriteLine(result);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
Task.WhenAllはすべての完了を待ってから結果を扱いますが、この方法なら完了したものから順次処理できます。
6-4. タイムアウト処理にWhenAnyを使う方法
対象処理とTask.Delayを競わせることで、タイムアウトを実装できます。
C#Task<string> operationTask = GetDataAsync();
Task timeoutTask = Task.Delay(
TimeSpan.FromSeconds(5));
Task completedTask = await Task.WhenAny(
operationTask,
timeoutTask);
if (completedTask == timeoutTask)
{
Console.WriteLine("タイムアウトしました");
}
else
{
string result = await operationTask;
Console.WriteLine(result);
}
ただし、この方法だけではタイムアウト後も元の処理が継続する可能性があります。可能であれば、CancellationTokenも渡して実際の処理をキャンセルします。
6-5. WhenAllとWhenAnyの使い分け
すべての処理結果が必要ならTask.WhenAllを使います。
C#await Task.WhenAll(tasks);
最初に完了した結果だけを使いたい場合や、完了順に処理したい場合はTask.WhenAnyを使います。
C#Task completedTask = await Task.WhenAny(tasks);
タイムアウト判定、複数サーバーへの問い合わせ、最初に得られた有効な結果の採用などでは、WhenAnyが適しています。
7. Taskの例外処理
7-1. awaitしたTaskの例外をtry-catchで捕捉する方法
非同期メソッド内で発生した例外は、呼び出し側でawaitしたときに再スローされます。
C#static async Task ExecuteAsync()
{
await Task.Delay(100);
throw new InvalidOperationException(
"処理に失敗しました");
}
try
{
await ExecuteAsync();
}
catch (InvalidOperationException ex)
{
Console.WriteLine(ex.Message);
}
同期処理と同じ感覚でtry-catchを利用できます。
7-2. Task.Run内で発生した例外の扱い
Task.Run内で発生した例外も、そのTaskをawaitしたときに捕捉できます。
C#try
{
await Task.Run(() =>
{
throw new ApplicationException(
"計算に失敗しました");
});
}
catch (ApplicationException ex)
{
Console.WriteLine(ex.Message);
}
Task.Runの呼び出し時点ではなく、返されたTaskをawaitした時点で例外が表面化します。
7-3. Task.WhenAllで複数の処理が失敗した場合の例外
複数のTaskが失敗する可能性がある場合は、WhenAllの戻り値を変数へ保存します。
C#Task task1 = FailAsync("エラー1");
Task task2 = FailAsync("エラー2");
Task allTask = Task.WhenAll(task1, task2);
try
{
await allTask;
}
catch (Exception ex)
{
Console.WriteLine(
$"awaitで捕捉: {ex.Message}");
if (allTask.Exception is not null)
{
foreach (Exception inner
in allTask.Exception.InnerExceptions)
{
Console.WriteLine(inner.Message);
}
}
}
個別の処理について、どの入力が失敗したか分かるように、例外へ情報を追加する設計も重要です。
7-4. AggregateExceptionが登場するケース
Task.Wait()やTask<TResult>.Resultなど、同期的にTaskを待つAPIでは、例外がAggregateExceptionに包まれることがあります。
C#try
{
Task task = ExecuteAsync();
task.Wait();
}
catch (AggregateException ex)
{
foreach (Exception inner
in ex.InnerExceptions)
{
Console.WriteLine(inner.Message);
}
}
一方、awaitは一般的に元の例外を再スローするため、通常の例外型で捕捉できます。非同期コードではWaitやResultではなく、awaitを使う方が例外処理も自然です。 Microsoft Learn
7-5. awaitしないTaskで例外を見落とす危険性
次のコードでは、返されたTaskを待っていません。
C#SaveAsync();
処理が失敗しても、その場のtry-catchでは捕捉できません。
C#try
{
SaveAsync();
}
catch
{
// SaveAsync内の非同期例外は
// 通常ここでは捕捉できない
}
原則として、非同期メソッドはawaitします。
C#try
{
await SaveAsync();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
7-6. 非同期メソッドの呼び出し元まで例外を伝える方法
途中のメソッドで例外を処理できない場合は、無理に捕捉せず、Taskを通じて上位へ伝えます。
C#static async Task<string> ServiceAsync()
{
return await RepositoryAsync();
}
static async Task<string> RepositoryAsync()
{
await Task.Delay(100);
throw new InvalidOperationException(
"データ取得に失敗しました");
}
上位層でログやユーザー向けメッセージを処理します。
C#try
{
string result = await ServiceAsync();
}
catch (InvalidOperationException ex)
{
logger.LogError(ex, "処理失敗");
}
再スローする場合は、スタックトレースを維持するためにthrow;を使います。
C#catch (Exception ex)
{
logger.LogError(ex, "エラー");
throw;
}
8. CancellationTokenでTaskをキャンセルする方法
8-1. 非同期処理にキャンセル機能が必要な理由
ユーザーが画面を閉じた場合や、HTTPリクエストが切断された場合、不要になった処理を継続すると、CPU、メモリ、ネットワーク、データベース接続を無駄に消費します。
CancellationTokenを使うと、呼び出し元から処理へキャンセル要求を伝えられます。
キャンセルは強制終了ではなく、呼び出し元と処理側が協力して停止する仕組みです。 Microsoft Learn+1
8-2. CancellationTokenSourceの基本的な使い方
CancellationTokenSourceは、キャンセル要求を発行するためのクラスです。
C#using CancellationTokenSource cts = new();
Task task = DoWorkAsync(cts.Token);
cts.Cancel();
try
{
await task;
}
catch (OperationCanceledException)
{
Console.WriteLine("キャンセルされました");
}
処理側にはctsそのものではなく、通常はcts.Tokenを渡します。
8-3. CancellationTokenを非同期メソッドへ渡す方法
キャンセル可能なメソッドは、引数としてCancellationTokenを受け取ります。
C#static async Task DoWorkAsync(
CancellationToken cancellationToken)
{
for (int i = 0; i < 100; i++)
{
cancellationToken
.ThrowIfCancellationRequested();
await Task.Delay(
100,
cancellationToken);
Console.WriteLine(i);
}
}
呼び出し先の非同期APIがCancellationTokenを受け取る場合は、そのまま渡します。
8-4. ThrowIfCancellationRequestedでキャンセルを検出する方法
CPU処理やループ処理では、定期的にThrowIfCancellationRequestedを呼び出します。
C#static void Calculate(
CancellationToken cancellationToken)
{
for (long i = 0; i < long.MaxValue; i++)
{
if (i % 10_000 == 0)
{
cancellationToken
.ThrowIfCancellationRequested();
}
// 計算処理
}
}
キャンセルが要求されている場合、OperationCanceledExceptionがスローされます。
確認頻度が低すぎると停止が遅れ、高すぎると確認処理の負荷が増えるため、処理内容に応じて調整します。
8-5. Task.Delayをキャンセルする方法
Task.DelayにはCancellationTokenを渡せます。
C#static async Task WaitAsync(
CancellationToken cancellationToken)
{
await Task.Delay(
TimeSpan.FromSeconds(10),
cancellationToken);
}
待機中にキャンセルされると、待機時間の終了を待たずにOperationCanceledExceptionが発生します。 Microsoft Learn
8-6. Task.Runをキャンセルする場合の注意点
Task.RunにCancellationTokenを渡しても、実行中の処理が自動的に強制停止されるわけではありません。
C#await Task.Run(
() => Calculate(cancellationToken),
cancellationToken);
Task.Runへ渡すトークンは、処理が開始される前のキャンセルなどに利用されます。実行開始後に止めるには、処理内部でトークンを確認する必要があります。
C#static void Calculate(
CancellationToken cancellationToken)
{
for (int i = 0; i < 1_000_000; i++)
{
cancellationToken
.ThrowIfCancellationRequested();
// 計算
}
}
8-7. OperationCanceledExceptionの処理方法
キャンセルは、一般的な障害とは分けて扱います。
C#try
{
await DoWorkAsync(cancellationToken);
}
catch (OperationCanceledException)
when (cancellationToken.IsCancellationRequested)
{
Console.WriteLine(
"要求によりキャンセルされました");
}
catch (Exception ex)
{
Console.WriteLine(
$"処理に失敗しました: {ex.Message}");
}
キャンセルをエラーとして記録するかどうかは、アプリケーションの要件によります。ユーザー操作による正常なキャンセルなら、エラーログではなく情報ログとして扱うことがあります。
8-8. タイムアウトとキャンセルを組み合わせる方法
外部からのキャンセルとタイムアウトを組み合わせる場合は、リンクされたCancellationTokenSourceを作成します。
C#static async Task<string> GetDataWithTimeoutAsync(
HttpClient client,
string url,
CancellationToken cancellationToken)
{
using CancellationTokenSource timeoutCts =
new(TimeSpan.FromSeconds(5));
using CancellationTokenSource linkedCts =
CancellationTokenSource
.CreateLinkedTokenSource(
cancellationToken,
timeoutCts.Token);
return await client.GetStringAsync(
url,
linkedCts.Token);
}
呼び出し元のキャンセルか、5秒のタイムアウトのどちらかが発生すると、リンクされたトークンへキャンセルが通知されます。
9. C#の非同期処理で初心者が注意すべきポイント
9-1. Task.ResultやTask.Waitで処理をブロックしない
次のコードは、Taskが完了するまで現在のスレッドをブロックします。
C#string result = GetDataAsync().Result;
C#GetDataAsync().Wait();
非同期メソッド内では、原則としてawaitを使用します。
C#string result = await GetDataAsync();
awaitなら待機中にスレッドを解放でき、例外処理も自然になります。
9-2. ResultやWaitがデッドロックを引き起こす仕組み
UIアプリなどでは、await後の処理が元のUIコンテキストへ戻ろうとすることがあります。
一方、UIスレッドが.Resultや.Wait()でそのTaskの完了を待っていると、次の状態になります。
UIスレッドが
Taskの完了を待つTaskの継続処理がUIスレッドを待つ互いに待ち続ける
これが典型的なデッドロックです。
呼び出し階層を可能な限り非同期に統一し、「async all the way」の形にします。
C#private async void Button_Click(
object sender,
EventArgs e)
{
string result = await GetDataAsync();
}
9-3. イベントハンドラー以外ではasync voidを避ける
async voidは、呼び出し元が完了を待てず、例外も通常のTaskと同じ方法で捕捉できません。
次の書き方は避けます。
C#static async void SaveAsync()
{
await Task.Delay(100);
}
Taskを返すようにします。
C#static async Task SaveAsync()
{
await Task.Delay(100);
}
UIイベントハンドラーなど、戻り値の型をvoidにする必要がある場面ではasync voidを使用できます。 Microsoft Learn+1
9-4. 非同期メソッドの呼び出しをawaitし忘れない
次のコードでは、保存処理が終わる前にメソッドが先へ進む可能性があります。
C#SaveAsync();
Console.WriteLine("保存完了");
正しくはawaitします。
C#await SaveAsync();
Console.WriteLine("保存完了");
意図的に待たない場合でも、例外の記録やアプリ終了時の扱いを設計する必要があります。
9-5. fire-and-forgetで例外や処理結果を失わない
処理を開始したまま待たない方式は、fire-and-forgetと呼ばれます。
C#_ = SendNotificationAsync();
この方法では、処理の完了前に必要なオブジェクトが破棄されたり、例外を見落としたりする可能性があります。
バックグラウンド処理が必要なら、キューやホステッドサービスなど、処理のライフサイクルと例外を管理できる仕組みを利用します。
どうしても待たずに開始する場合は、例外をメソッド内部で記録します。
C#static async Task SendSafelyAsync()
{
try
{
await SendNotificationAsync();
}
catch (Exception ex)
{
logger.LogError(
ex,
"通知送信に失敗しました");
}
}
9-6. awaitを付けるだけでは並行実行にならない
次のコードは逐次実行です。
C#await ProcessAsync(1);
await ProcessAsync(2);
await ProcessAsync(3);
並行して開始するには、先にTaskを取得します。
C#Task task1 = ProcessAsync(1);
Task task2 = ProcessAsync(2);
Task task3 = ProcessAsync(3);
await Task.WhenAll(
task1,
task2,
task3);
ただし、並行実行によって共有リソースの競合や接続先への負荷が増える点に注意が必要です。
9-7. 共有データを複数のTaskから同時に更新しない
複数のTaskから同じ変数やコレクションを更新すると、競合状態が発生する可能性があります。
C#int total = 0;
Task[] tasks = Enumerable
.Range(1, 1000)
.Select(_ => Task.Run(() =>
{
total++;
}))
.ToArray();
await Task.WhenAll(tasks);
total++は不可分な処理ではないため、期待した値にならない可能性があります。
単純な数値更新ならInterlockedを利用できます。
C#Interlocked.Increment(ref total);
複雑な共有状態では、lock、SemaphoreSlim、スレッドセーフなコレクション、共有を避ける設計などを検討します。
9-8. Task.Runを非同期APIの代わりに多用しない
次のように、非同期APIをさらにTask.Runで包む必要はありません。
C#await Task.Run(
() => client.GetStringAsync(url));
次のように直接待ちます。
C#await client.GetStringAsync(url);
Task.Runは、主にCPU負荷の高い同期処理を呼び出し元のスレッドから切り離すための手段です。
9-9. Thread.SleepではなくTask.Delayを使う
Thread.Sleepは、指定時間だけ現在のスレッドをブロックします。
C#Thread.Sleep(1000);
非同期メソッドでは、Task.Delayを使います。
C#await Task.Delay(1000);
Task.Delayなら待機中にスレッドを占有せず、CancellationTokenも渡せます。
9-10. ConfigureAwait(false)が必要になる場面を理解する
awaitは、実行環境によっては元のコンテキストで後続処理を再開します。
C#await SomeOperationAsync()
.ConfigureAwait(false);
ConfigureAwait(false)を指定すると、元のコンテキストで再開する必要がないことを示せます。
UI部品を操作しない汎用ライブラリでは、コンテキストへの復帰を不要にする目的で使われることがあります。一方、UIコードではawait後に画面を更新するため、元のUIコンテキストへの復帰が必要です。
ASP.NET Coreには従来型のUI用SynchronizationContextがないため、アプリケーションコードで機械的にConfigureAwait(false)を付ける必要は通常ありません。利用するかどうかは、実行環境とライブラリの設計方針に合わせて判断します。 Microsoft Learn
10. Task・async/await・Task.Run・WhenAllの使い分け
10-1. Web APIやファイル操作などのI/O待ちはasync/awaitを使う
HTTP通信、ファイルアクセス、データベースアクセスなどでは、対象ライブラリが提供する非同期APIを利用します。
C#string text = await client.GetStringAsync(url);
C#string text = await File.ReadAllTextAsync(path);
I/O待ちをawaitすることで、待機中のスレッドをほかの処理へ利用できます。
10-2. CPU負荷の高い同期処理はTask.Runを検討する
画像変換、大量データの解析、複雑な計算など、CPUを長時間使用する同期処理ではTask.Runを検討します。
C#ProcessedData result = await Task.Run(
() => ProcessLargeData(data));
特にUIアプリでは、重い処理をUIスレッドから切り離す効果があります。
ASP.NET Coreでは、単純にTask.Runへ移すだけでサーバー全体の処理能力が上がるとは限らないため、処理量、同時実行数、バックグラウンド化の必要性を検討します。
10-3. 複数の独立した処理はTask.WhenAllで並行実行する
互いに依存しない処理は、先に開始してTask.WhenAllで待ちます。
C#Task<User> userTask = GetUserAsync();
Task<Order[]> ordersTask = GetOrdersAsync();
Task<Point> pointTask = GetPointAsync();
await Task.WhenAll(
userTask,
ordersTask,
pointTask);
すべてを順番に待つより、全体の待ち時間を短縮できる可能性があります。
10-4. 最初に完了した処理を使う場合はTask.WhenAnyを選ぶ
複数の候補のうち最も早い結果を使う場合は、Task.WhenAnyを利用します。
C#Task<string> firstTask =
await Task.WhenAny(
GetFromServerAAsync(),
GetFromServerBAsync());
string result = await firstTask;
残った処理が不要なら、CancellationTokenを使って停止できる設計にします。
10-5. 処理結果が不要な場合はTaskを返す
保存や通知など、完了だけを通知すればよいメソッドはTaskを返します。
C#static async Task SaveAsync()
{
await database.SaveAsync();
}
voidではなくTaskを返すことで、呼び出し元が完了、例外、キャンセルを扱えます。
10-6. 処理結果が必要な場合はTask<TResult>を返す
検索結果や計算結果が必要なら、Task<TResult>を返します。
C#static async Task<User?> FindUserAsync(
int id)
{
return await database.FindUserAsync(id);
}
呼び出し側はawaitによってUserを取得できます。
10-7. 用途別に選べる非同期処理の判断フロー
処理方法に迷った場合は、次の順序で考えます。
まず、処理がI/O待ちで、非同期APIが用意されているなら、そのAPIをawaitします。
C#await client.GetAsync(url);
CPU負荷の高い同期処理をUIスレッドから切り離したいなら、Task.Runを検討します。
C#await Task.Run(() => HeavyCalculation());
複数の処理が独立しており、すべて必要ならTask.WhenAllを使います。
C#await Task.WhenAll(tasks);
最初に完了した処理だけを使うならTask.WhenAnyを使います。
C#Task first = await Task.WhenAny(tasks);
処理を中断できるようにするならCancellationTokenを受け取ります。
C#await ExecuteAsync(cancellationToken);
11. Taskを使った実践的なサンプル
11-1. 非同期でWeb APIからデータを取得する例
HttpClientを使ってWeb APIから文字列を取得する例です。
C#static async Task<string> GetApiDataAsync(
HttpClient client,
string url,
CancellationToken cancellationToken)
{
using HttpResponseMessage response =
await client.GetAsync(
url,
cancellationToken);
response.EnsureSuccessStatusCode();
return await response.Content
.ReadAsStringAsync(cancellationToken);
}
呼び出し側は次のようになります。
C#using HttpClient client = new();
try
{
string json = await GetApiDataAsync(
client,
"https://example.com/api/items",
CancellationToken.None);
Console.WriteLine(json);
}
catch (HttpRequestException ex)
{
Console.WriteLine(
$"通信エラー: {ex.Message}");
}
ASP.NET Coreなどでは、毎回HttpClientを生成するのではなく、通常はIHttpClientFactoryなどを利用して管理します。
11-2. 複数のWeb APIをTask.WhenAllで並行取得する例
複数のAPIが独立している場合は、並行して取得できます。
C#static async Task<string[]> GetAllDataAsync(
HttpClient client,
CancellationToken cancellationToken)
{
Task<string> userTask =
client.GetStringAsync(
"https://example.com/api/user",
cancellationToken);
Task<string> orderTask =
client.GetStringAsync(
"https://example.com/api/orders",
cancellationToken);
Task<string> newsTask =
client.GetStringAsync(
"https://example.com/api/news",
cancellationToken);
return await Task.WhenAll(
userTask,
orderTask,
newsTask);
}
結果配列は、userTask、orderTask、newsTaskの順に格納されます。
11-3. Task.Runで重い計算処理を実行する例
素数の個数を数えるCPU処理を、Task.Runで実行します。
C#static int CountPrimes(
int max,
CancellationToken cancellationToken)
{
int count = 0;
for (int number = 2;
number <= max;
number++)
{
cancellationToken
.ThrowIfCancellationRequested();
bool isPrime = true;
int limit = (int)Math.Sqrt(number);
for (int divisor = 2;
divisor <= limit;
divisor++)
{
if (number % divisor == 0)
{
isPrime = false;
break;
}
}
if (isPrime)
{
count++;
}
}
return count;
}
static Task<int> CountPrimesAsync(
int max,
CancellationToken cancellationToken)
{
return Task.Run(
() => CountPrimes(
max,
cancellationToken),
cancellationToken);
}
呼び出し側では次のように待ちます。
C#int count = await CountPrimesAsync(
1_000_000,
cancellationToken);
Console.WriteLine(
$"素数の個数: {count}");
11-4. 進捗を表示しながら非同期処理を実行する例
IProgress<T>を使うと、非同期処理から進捗を通知できます。
C#static async Task ProcessItemsAsync(
IReadOnlyList<string> items,
IProgress<int>? progress,
CancellationToken cancellationToken)
{
for (int i = 0; i < items.Count; i++)
{
cancellationToken
.ThrowIfCancellationRequested();
await ProcessItemAsync(
items[i],
cancellationToken);
int percent =
(i + 1) * 100 / items.Count;
progress?.Report(percent);
}
}
UI側では次のように利用できます。
C#var progress = new Progress<int>(
value =>
{
progressBar.Value = value;
progressLabel.Text = $"{value}%";
});
await ProcessItemsAsync(
items,
progress,
cancellationToken);
Progress<T>は、作成されたコンテキストに応じて進捗コールバックを実行するため、UIの更新にも利用できます。
11-5. キャンセルとタイムアウトに対応した処理例
外部キャンセルとタイムアウトの両方に対応した例です。
C#static async Task<string> DownloadAsync(
HttpClient client,
string url,
TimeSpan timeout,
CancellationToken cancellationToken)
{
using CancellationTokenSource timeoutCts =
new(timeout);
using CancellationTokenSource linkedCts =
CancellationTokenSource
.CreateLinkedTokenSource(
cancellationToken,
timeoutCts.Token);
try
{
return await client.GetStringAsync(
url,
linkedCts.Token);
}
catch (OperationCanceledException)
when (timeoutCts.IsCancellationRequested
&& !cancellationToken
.IsCancellationRequested)
{
throw new TimeoutException(
$"処理が{timeout.TotalSeconds}秒以内に完了しませんでした");
}
}
タイムアウトとユーザーによるキャンセルを区別している点がポイントです。
11-6. 例外処理を含む実務向けの非同期メソッド例
通信エラー、タイムアウト、キャンセルを考慮した例です。
C#static async Task<string?> LoadDataAsync(
HttpClient client,
string url,
ILogger logger,
CancellationToken cancellationToken)
{
using CancellationTokenSource timeoutCts =
new(TimeSpan.FromSeconds(10));
using CancellationTokenSource linkedCts =
CancellationTokenSource
.CreateLinkedTokenSource(
cancellationToken,
timeoutCts.Token);
try
{
using HttpResponseMessage response =
await client.GetAsync(
url,
linkedCts.Token);
response.EnsureSuccessStatusCode();
return await response.Content
.ReadAsStringAsync(
linkedCts.Token);
}
catch (OperationCanceledException)
when (cancellationToken
.IsCancellationRequested)
{
logger.LogInformation(
"データ取得がキャンセルされました");
throw;
}
catch (OperationCanceledException)
when (timeoutCts
.IsCancellationRequested)
{
logger.LogWarning(
"データ取得がタイムアウトしました");
throw new TimeoutException(
"データ取得がタイムアウトしました");
}
catch (HttpRequestException ex)
{
logger.LogError(
ex,
"Web APIへの接続に失敗しました");
return null;
}
}
実務では、すべての例外を無条件で握りつぶさず、上位層で対応すべき例外は再スローします。
12. C# Taskに関するよくある質問
12-1. Taskを作成しただけで処理は開始される?
作成方法によって異なります。
Task.Runで作成したTaskは、スレッドプールへ登録され、実行開始を待ちます。
C#Task task = Task.Run(() => DoWork());
asyncメソッドは呼び出した時点で実行が始まり、最初の完了していないawaitまで同期的に進みます。
C#Task task = DoWorkAsync();
一方、new Taskで作成しただけでは開始されません。
C#Task task = new(() => DoWork());
Console.WriteLine(task.Status); // Created
task.Start();
通常のアプリケーションコードでは、new TaskよりTask.Runや非同期APIを利用することが一般的です。 Microsoft Learn
12-2. asyncメソッドは必ず別スレッドで動く?
必ず別スレッドで動くわけではありません。
asyncは、メソッド内でawaitを利用できるようにするキーワードです。メソッドの開始部分は呼び出し元のスレッドで実行されることがあります。
また、I/O待ちの間に、その処理専用のスレッドが存在するとは限りません。await後の処理がどのスレッドで再開されるかは、実行環境やコンテキストによって異なります。
12-3. Task.Runと新しいThreadを作る方法は何が違う?
Task.Runは通常、.NETのスレッドプールを利用します。スレッドを再利用するため、短時間の処理を効率的に実行できます。
C#await Task.Run(() => DoWork());
Threadを直接作成すると、専用のOSスレッドを作成します。
C#Thread thread = new(DoWork);
thread.Start();
長時間動作する専用スレッド、スレッドの優先順位、アパートメント状態などを細かく制御する必要がある特殊なケースを除き、一般的な非同期処理ではTaskを利用します。
12-4. awaitを複数回書くと処理は同時に動く?
次のように書くと、基本的には順番に実行されます。
C#await Work1Async();
await Work2Async();
同時に進めたい場合は、先に両方の処理を開始します。
C#Task task1 = Work1Async();
Task task2 = Work2Async();
await Task.WhenAll(task1, task2);
12-5. Task.WhenAllの実行順と結果の順番は同じ?
各処理が完了する順番は保証されません。
ただし、Task<TResult>をWhenAllしたときの結果配列は、入力したTaskの順番に対応します。
C#Task<int> task1 = GetValueAsync(1);
Task<int> task2 = GetValueAsync(2);
int[] results = await Task.WhenAll(
task1,
task2);
task2が先に完了しても、results[0]はtask1の結果、results[1]はtask2の結果です。
12-6. Task.Runをawaitしないとどうなる?
Task.Runへ登録した処理は進行しますが、呼び出し元は完了を待ちません。
C#Task.Run(() => DoWork());
その結果、次の問題が起こる可能性があります。
処理完了前にアプリケーションが終了する
例外を見落とす
処理結果を取得できない
利用しているオブジェクトが先に破棄される
同じ処理が重複して開始される
通常は返されたTaskをawaitします。
C#await Task.Run(() => DoWork());
12-7. async voidを使ってよいのはどのような場合?
主な利用場面は、UIのイベントハンドラーです。
C#private async void Button_Click(
object sender,
EventArgs e)
{
await SaveAsync();
}
イベントハンドラー以外の非同期メソッドでは、原則としてTaskまたはTask<TResult>を返します。
12-8. 非同期処理は必ず処理時間を短縮できる?
必ず短縮できるわけではありません。
非同期処理の主な目的は、待機中にスレッドを占有しないことや、UIの応答性、Webサーバーの同時処理能力を高めることです。
1件のHTTP通信そのものが、awaitによって速くなるわけではありません。複数の独立した通信をTask.WhenAllで並行実行した場合は、全体の待ち時間を短縮できる可能性があります。
一方、処理の分割やスケジューリングによるオーバーヘッドがあるため、非常に短い処理を無理に非同期化すると、かえって効率が落ちる場合があります。
12-9. Taskを安全に使うために最低限守るべきことは?
最低限、次の点を守ることが重要です。
非同期メソッドが返すTaskは、原則としてawaitします。
C#await SaveAsync();
.Resultや.Wait()による同期ブロックを避けます。
C#string result = await GetDataAsync();
イベントハンドラー以外ではasync voidを使いません。
C#async Task ExecuteAsync()
{
await DoWorkAsync();
}
I/O処理では非同期APIを使い、安易にTask.Runで包みません。
複数の独立した処理は、負荷を考慮したうえでTask.WhenAllを利用します。
キャンセルが必要な処理にはCancellationTokenを渡し、例外はtry-catchまたは上位層への伝播によって確実に扱います。
まとめ
C#のTaskは、非同期処理の進行状況、完了、結果、例外、キャンセルを表すためのクラスです。戻り値が不要ならTask、戻り値が必要ならTask<TResult>を使います。
asyncはメソッド内でawaitを利用できるようにし、awaitはTaskの完了をスレッドをブロックせずに待つために使います。
Web API、ファイル、データベースなどのI/O待ちでは、非同期APIを直接awaitするのが基本です。CPU負荷の高い同期処理をUIスレッドから切り離したい場合は、Task.Runを検討します。
複数の独立した処理をすべて待つ場合はTask.WhenAll、最初に完了した処理を扱う場合はTask.WhenAnyを利用します。
また、非同期処理では、例外の見落としやデッドロックを防ぐために、Task.ResultやTask.Wait()を避け、返されたTaskを確実にawaitすることが重要です。必要に応じてCancellationTokenを受け取り、処理を安全にキャンセルできるように設計しましょう。

