C#のasync/awaitとは?初心者がつまずく非同期処理の仕組み・使い方・注意点をサンプルで解説
はじめに
C#でアプリケーションを作っていると、ファイル読み込み、Web API呼び出し、データベースアクセス、待機処理などで「処理が終わるまで画面が固まる」「レスポンスが遅くなる」といった問題に出会うことがあります。
そこで重要になるのが、C#のasyncとawaitです。
async/awaitは、C#で非同期処理をわかりやすく書くための仕組みです。非同期処理と聞くと難しく感じるかもしれませんが、基本的な考え方は「時間のかかる処理を待っている間、プログラム全体を止めないようにする」ことです。
この記事では、C#のasync/awaitについて、初心者がつまずきやすいポイントを中心に、Taskの基本、使い方、注意点、Task.Runとの違い、複数の非同期処理の扱い方までサンプルコード付きで解説します。
1. C#のasync/awaitとは?非同期処理を初心者向けにわかりやすく解説
C#のasync/awaitは、非同期処理を通常の処理の流れに近い形で書ける構文です。
非同期処理とは、時間のかかる処理が完了するまで、プログラム全体を止めずに進められる処理のことです。
たとえば、Web APIからデータを取得する処理を考えてみましょう。通信には数秒かかることがあります。その間、同期処理では呼び出し元が待ち続けます。一方、非同期処理では、待っている間に他の処理へ制御を戻せます。
C#では、この非同期処理を扱いやすくするためにasyncとawaitを使います。
1-1. async/awaitは「処理を待つ間に別の処理を進める」ための仕組み
async/awaitを使うと、時間のかかる処理を待っている間に、スレッドを無駄にブロックしにくくなります。
たとえば、次のような処理があるとします。
C#Console.WriteLine("処理開始");
await Task.Delay(3000);
Console.WriteLine("処理終了");
Task.Delay(3000)は3秒待つ処理です。awaitを付けることで、「この処理が終わるまで待つが、その間にスレッドをブロックし続けない」という書き方になります。
見た目は上から順番に実行されるように見えますが、内部的には待機中に呼び出し元へ制御を返すことができます。
1-2. 同期処理と非同期処理の違い
同期処理は、1つの処理が終わるまで次の処理に進まない方式です。
C#Console.WriteLine("開始");
Thread.Sleep(3000);
Console.WriteLine("終了");
このコードでは、Thread.Sleep(3000)の間、現在のスレッドは完全に停止します。UIアプリであれば画面が固まり、Webアプリであればリクエスト処理の効率が悪くなる可能性があります。
一方、非同期処理では次のように書けます。
C#Console.WriteLine("開始");
await Task.Delay(3000);
Console.WriteLine("終了");
Task.Delayは待機を表す非同期処理です。awaitによって処理の完了を待ちますが、Thread.Sleepのようにスレッドを占有し続けるわけではありません。
つまり、同期処理は「終わるまでその場で待つ」、非同期処理は「終わるまで待つが、待っている間にリソースを有効活用できる」と考えると理解しやすいです。
1-3. asyncとawaitを使うと何が便利になるのか
async/awaitを使う最大のメリットは、非同期処理を読みやすく書けることです。
async/awaitがない場合、非同期処理はコールバックや継続処理を使って書く必要があり、処理の流れが追いにくくなりがちです。
async/awaitを使うと、次のように通常の処理に近い形で書けます。
C#var user = await GetUserAsync();
var orders = await GetOrdersAsync(user.Id);
Console.WriteLine($"注文数: {orders.Count}");
このコードは、ユーザー情報を取得し、そのユーザーIDを使って注文情報を取得する流れを自然に読めます。
非同期処理でありながら、同期処理のような見た目で書けることがasync/awaitの大きな利点です。
1-4. C#でasync/awaitがよく使われる場面
C#でasync/awaitがよく使われるのは、主に時間がかかるI/O処理です。
代表的な例は次のような処理です。
C#// Web API呼び出し
var json = await httpClient.GetStringAsync("https://example.com/api/users");
// ファイル読み込み
var text = await File.ReadAllTextAsync("sample.txt");
// データベースアクセス
var users = await dbContext.Users.ToListAsync();
Web通信、ファイル読み書き、データベースアクセスなどは、CPUで計算している時間よりも、外部リソースの応答を待っている時間が長くなりやすい処理です。
そのため、async/awaitとの相性が非常に良いです。
2. async/awaitを理解する前に知っておきたいTaskの基本
async/awaitを理解するには、まずTaskを知っておく必要があります。
C#の非同期メソッドでは、多くの場合、戻り値としてTaskまたはTask<T>を使います。Taskは「将来完了する処理」を表す型です。
2-1. Taskとは何か
Taskは、非同期処理の状態を表すオブジェクトです。
たとえば、次のメソッドは3秒後に完了する処理を表します。
C#Task task = Task.Delay(3000);
この時点でtaskは「3秒後に完了する予定の処理」を表しています。
awaitを使うと、そのTaskが完了するまで待てます。
C#await task;
つまり、Taskは処理そのものというより、「進行中または完了予定の処理を表すもの」と考えるとわかりやすいです。
2-2. TaskとTask<T>の違い
TaskとTask<T>の違いは、戻り値があるかどうかです。
戻り値がない非同期処理ではTaskを使います。
C#async Task SaveAsync()
{
await Task.Delay(1000);
Console.WriteLine("保存しました");
}
戻り値がある非同期処理ではTask<T>を使います。
C#async Task<int> GetCountAsync()
{
await Task.Delay(1000);
return 10;
}
この場合、Task<int>は「将来的にint型の結果を返す非同期処理」を意味します。
呼び出し側では、awaitするとintの値を受け取れます。
C#int count = await GetCountAsync();
Console.WriteLine(count);
2-3. void・Task・Task<T>の使い分け
C#のメソッドでは、戻り値としてvoid、Task、Task<T>を使い分けます。
通常の同期メソッドで戻り値がない場合はvoidを使います。
C#void ShowMessage()
{
Console.WriteLine("Hello");
}
非同期メソッドで戻り値がない場合はTaskを使います。
C#async Task ShowMessageAsync()
{
await Task.Delay(1000);
Console.WriteLine("Hello");
}
非同期メソッドで戻り値がある場合はTask<T>を使います。
C#async Task<string> GetMessageAsync()
{
await Task.Delay(1000);
return "Hello";
}
基本的には、非同期メソッドではvoidではなくTaskまたはTask<T>を使うと覚えておきましょう。
async voidはイベントハンドラーなど一部の例外的な場面を除き、避けるべきです。
2-4. awaitできる処理とできない処理
awaitできるのは、基本的にTaskやTask<T>などの「await可能な型」です。
たとえば、次のコードはawaitできます。
C#await Task.Delay(1000);
次のように、Task<T>を返すメソッドもawaitできます。
C#string result = await GetMessageAsync();
一方、普通の値はawaitできません。
C#int number = 10;
// await number; // コンパイルエラー
awaitは「時間のかかる処理の完了を待つ」ための構文であり、通常の値に対して使うものではありません。
3. C#におけるasync/awaitの基本的な使い方
ここからは、C#でasync/awaitを使う基本的な書き方を見ていきます。
基本形は、メソッドにasyncを付け、非同期処理の前にawaitを書く形です。
3-1. asyncメソッドの書き方
非同期メソッドを定義するには、メソッドの戻り値の前にasyncを付けます。
C#async Task DoWorkAsync()
{
await Task.Delay(1000);
Console.WriteLine("処理完了");
}
ポイントは、asyncを付けたメソッドの中でawaitを使えるようになることです。
ただし、asyncを付けただけで処理が自動的に別スレッドで動くわけではありません。asyncは、メソッド内でawaitを使うための宣言だと考えると理解しやすいです。
3-2. awaitの基本構文
awaitは、Taskが完了するまで待つために使います。
C#await 非同期処理;
具体的には次のように書きます。
C#await Task.Delay(1000);
戻り値がある場合は、awaitした結果を変数に代入できます。
C#string message = await GetMessageAsync();
awaitを書くと、その処理が完了するまで次の行には進みません。ただし、同期処理のようにスレッドを占有し続けるわけではありません。
3-3. 戻り値がない非同期メソッドのサンプル
戻り値がない非同期メソッドはTaskを返します。
C#async Task PrintAsync()
{
Console.WriteLine("開始");
await Task.Delay(1000);
Console.WriteLine("終了");
}
呼び出し側では次のようにawaitします。
C#await PrintAsync();
このように、処理結果を返さない場合でも、非同期メソッドではvoidではなくTaskを返すのが基本です。
3-4. 戻り値がある非同期メソッドのサンプル
戻り値がある場合はTask<T>を使います。
C#async Task<string> GetUserNameAsync()
{
await Task.Delay(1000);
return "Taro";
}
呼び出し側では、awaitするとstringとして結果を受け取れます。
C#string name = await GetUserNameAsync();
Console.WriteLine(name);
Task<string>は、awaitすることでstringになります。
この感覚をつかむと、Task<T>の意味が理解しやすくなります。
3-5. Mainメソッドでasync/awaitを使う方法
C#では、Mainメソッドをasyncにできます。
C#class Program
{
static async Task Main(string[] args)
{
Console.WriteLine("開始");
await Task.Delay(1000);
Console.WriteLine("終了");
}
}
コンソールアプリで非同期処理を試す場合は、この書き方が便利です。
以前はMainメソッドで直接awaitできない環境もありましたが、現在のC#ではstatic async Task Mainを使うのが一般的です。
4. サンプルコードで学ぶasync/awaitの動き
async/awaitは、実際のコードを見ながら理解するとわかりやすくなります。
ここでは、同期処理と非同期処理の違い、awaitを書いたときの実行順序を確認していきます。
4-1. 同期処理のサンプルコード
まずは同期処理の例です。
C#using System;
using System.Threading;
class Program
{
static void Main()
{
Console.WriteLine("1. 開始");
DoWork();
Console.WriteLine("3. 終了");
}
static void DoWork()
{
Console.WriteLine("2. 処理中");
Thread.Sleep(3000);
}
}
このコードでは、DoWorkの中で3秒間スレッドが停止します。
実行結果は次のようになります。
1. 開始
2. 処理中
3. 終了
Thread.Sleepの間、現在のスレッドは何もできません。
4-2. 非同期処理のサンプルコード
次に、非同期処理の例です。
C#using System;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
Console.WriteLine("1. 開始");
await DoWorkAsync();
Console.WriteLine("3. 終了");
}
static async Task DoWorkAsync()
{
Console.WriteLine("2. 処理中");
await Task.Delay(3000);
}
}
実行結果は同期処理と同じように見えます。
1. 開始
2. 処理中
3. 終了
ただし、内部の動きは異なります。
Task.Delayはスレッドを停止させるのではなく、指定した時間が経過したら処理を再開する非同期処理です。
4-3. 実行順序から見るawaitの挙動
awaitの挙動を理解するには、「awaitした場所で一度処理が中断され、Taskが完了したら続きから再開される」と考えるとわかりやすいです。
C#static async Task Main()
{
Console.WriteLine("A");
await WaitAsync();
Console.WriteLine("D");
}
static async Task WaitAsync()
{
Console.WriteLine("B");
await Task.Delay(1000);
Console.WriteLine("C");
}
実行結果は次のようになります。
A
B
C
D
MainはWaitAsyncをawaitしているため、WaitAsyncが完了してからDを出力します。
一方、WaitAsyncの中ではTask.Delayの完了を待ち、完了後にCを出力します。
4-4. Task.Delayを使った非同期処理の例
Task.Delayは、非同期処理の動きを学ぶときによく使われます。
C#async Task CountdownAsync()
{
Console.WriteLine("3");
await Task.Delay(1000);
Console.WriteLine("2");
await Task.Delay(1000);
Console.WriteLine("1");
await Task.Delay(1000);
Console.WriteLine("Start!");
}
このコードは1秒ごとにカウントダウンします。
Thread.Sleepと違い、Task.Delayは非同期的な待機なので、UIアプリやサーバーアプリでスレッドを無駄に止めにくいというメリットがあります。
4-5. Web API呼び出しでasync/awaitを使う例
実務でよくあるのが、HttpClientを使ったWeb API呼び出しです。
C#using System;
using System.Net.Http;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
string result = await GetHtmlAsync("https://example.com");
Console.WriteLine(result);
}
static async Task<string> GetHtmlAsync(string url)
{
using var client = new HttpClient();
string html = await client.GetStringAsync(url);
return html;
}
}
GetStringAsyncは非同期でHTTP通信を行うメソッドです。
通信が完了するまで待つ必要がありますが、その間にスレッドをブロックし続ける必要はありません。
5. async/awaitで初心者がつまずきやすいポイント
async/awaitは便利ですが、初心者が誤解しやすいポイントも多いです。
特に、awaitの書き忘れ、asyncの意味、async void、Task.ResultやWait()の使い方には注意が必要です。
5-1. awaitを書き忘れるとどうなるか
非同期メソッドを呼び出すときにawaitを書き忘れると、処理の完了を待たずに次の行へ進んでしまいます。
C#static async Task Main()
{
DoWorkAsync();
Console.WriteLine("終了");
}
static async Task DoWorkAsync()
{
await Task.Delay(1000);
Console.WriteLine("処理完了");
}
この場合、DoWorkAsync()を呼び出していますが、awaitしていません。
そのため、DoWorkAsyncの完了を待たずにConsole.WriteLine("終了")が実行されます。
正しくは次のように書きます。
C#static async Task Main()
{
await DoWorkAsync();
Console.WriteLine("終了");
}
非同期処理の結果や完了を待ちたい場合は、必ずawaitを書きましょう。
5-2. asyncを付けただけでは非同期処理にならない
asyncを付けただけで、メソッドが自動的に非同期になるわけではありません。
C#async Task DoWorkAsync()
{
Console.WriteLine("処理中");
}
このメソッドにはasyncが付いていますが、awaitがありません。
この場合、実質的には同期的に実行されます。
asyncは「このメソッド内でawaitを使えるようにする」ためのキーワードです。非同期処理そのものを発生させるわけではありません。
5-3. async voidを避けるべき理由
async voidは、基本的に避けるべきです。
C#async void DoWorkAsync()
{
await Task.Delay(1000);
throw new Exception("エラー");
}
async voidにすると、呼び出し側でawaitできません。
C#DoWorkAsync(); // awaitできない
そのため、処理の完了を待つことも、例外を適切に捕まえることも難しくなります。
通常は次のようにTaskを返します。
C#async Task DoWorkAsync()
{
await Task.Delay(1000);
}
ただし、ボタンクリックなどのイベントハンドラーではasync voidを使うことがあります。
C#private async void Button_Click(object sender, EventArgs e)
{
await SaveAsync();
}
イベントハンドラー以外では、async voidではなくasync Taskを使うのが基本です。
5-4. Task.ResultやWait()でデッドロックが起きる理由
非同期処理の結果を同期的に取り出すために、Task.ResultやWait()を使いたくなることがあります。
C#var result = GetDataAsync().Result;
または次のようなコードです。
C#GetDataAsync().Wait();
しかし、これらはデッドロックやスレッドのブロックを引き起こす原因になります。
特にUIアプリや古いASP.NET環境では、await後の処理が元のコンテキストに戻ろうとする一方で、そのコンテキストがResultやWait()によってブロックされていると、処理が進まなくなることがあります。
基本的には、非同期処理は最後までawaitでつなげるようにしましょう。
C#var result = await GetDataAsync();
「async all the way」という考え方が大切です。
5-5. 例外処理の書き方を間違えやすい
async/awaitでも、基本的な例外処理はtry-catchで書けます。
C#try
{
string result = await GetDataAsync();
Console.WriteLine(result);
}
catch (Exception ex)
{
Console.WriteLine($"エラー: {ex.Message}");
}
awaitしている場合、非同期処理内で発生した例外はcatchで捕まえられます。
ただし、awaitを書き忘れると、想定どおりに例外を捕まえられないことがあります。
C#try
{
GetDataAsync(); // awaitしていない
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
この場合、非同期処理の中で後から発生した例外は、このcatchで捕まえられない可能性があります。
例外処理を正しく行うためにも、必要な非同期処理にはawaitを付けましょう。
6. async/awaitの注意点とベストプラクティス
async/awaitを実務で使うときは、いくつかのルールを意識すると、読みやすく安全なコードになります。
ここでは、非同期メソッド名、I/O処理、CPU負荷の高い処理、キャンセル、例外処理などのベストプラクティスを紹介します。
6-1. 非同期メソッド名にはAsyncを付ける
非同期メソッドには、名前の末尾にAsyncを付けるのが一般的です。
C#Task SaveAsync()
Task LoadUsersAsync()
Task<string> GetMessageAsync()
Asyncを付けることで、呼び出し側が「このメソッドは非同期で、awaitする必要がある」と判断しやすくなります。
C#await SaveAsync();
メソッド名から非同期処理だとわかるようにすることは、可読性と保守性の向上につながります。
6-2. I/O処理ではasync/awaitを積極的に使う
Web API、ファイル、データベースなどのI/O処理では、async/awaitを積極的に使うのがおすすめです。
C#string text = await File.ReadAllTextAsync("sample.txt");
C#var response = await httpClient.GetAsync("https://example.com");
C#var users = await dbContext.Users.ToListAsync();
I/O処理は待ち時間が発生しやすいため、非同期化することでスレッドを効率よく使えるようになります。
6-3. CPU負荷の高い処理ではTask.Runを検討する
画像処理、大量データの計算、暗号化、圧縮など、CPUを長時間使う処理ではTask.Runを検討することがあります。
C#int result = await Task.Run(() =>
{
return HeavyCalculation();
});
Task.Runを使うと、処理をスレッドプール上で実行できます。
ただし、何でもTask.Runで囲めばよいわけではありません。I/O処理には、専用の非同期APIを使う方が適切です。
6-4. ConfigureAwait(false)が必要になるケース
ライブラリ開発などでは、ConfigureAwait(false)を使うことがあります。
C#var result = await GetDataAsync().ConfigureAwait(false);
通常、await後の処理は、元のコンテキストに戻ろうとします。UIアプリでは、画面更新のためにUIスレッドへ戻る必要があります。
一方、ライブラリ内部の処理では、必ずしも元のコンテキストに戻る必要がありません。そのような場合にConfigureAwait(false)を使うと、不要なコンテキスト復帰を避けられることがあります。
ただし、UIを更新する処理でConfigureAwait(false)を使うと、UIスレッド以外から画面を操作してエラーになることがあります。
初心者のうちは、まず通常のawaitを理解し、必要になったらConfigureAwait(false)を学ぶとよいでしょう。
6-5. CancellationTokenでキャンセルに対応する
時間のかかる非同期処理では、キャンセルに対応することも重要です。
C#ではCancellationTokenを使ってキャンセルを表現します。
C#async Task DownloadAsync(CancellationToken cancellationToken)
{
await Task.Delay(5000, cancellationToken);
Console.WriteLine("ダウンロード完了");
}
呼び出し側では、CancellationTokenSourceを使います。
C#using var cts = new CancellationTokenSource();
try
{
await DownloadAsync(cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("キャンセルされました");
}
ユーザーがキャンセルボタンを押した場合や、一定時間でタイムアウトさせたい場合に便利です。
6-6. 例外はtry-catchで正しく扱う
非同期処理でも、例外はtry-catchで扱います。
C#try
{
await SaveAsync();
}
catch (IOException ex)
{
Console.WriteLine($"ファイルエラー: {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"予期しないエラー: {ex.Message}");
}
重要なのは、例外が発生する可能性がある非同期処理をawaitすることです。
awaitしないまま処理を放置すると、例外の発生に気づきにくくなります。
7. async/awaitとTask.Runの違い
async/awaitとTask.Runは混同されやすいですが、役割が異なります。
async/awaitは非同期処理を扱いやすくする構文です。一方、Task.Runは処理を別スレッドで実行するためのメソッドです。
7-1. async/awaitは非同期処理を扱いやすくする構文
async/awaitは、非同期処理の完了を自然な形で待つための構文です。
C#string result = await GetDataAsync();
このように書くことで、非同期処理の完了後に次の処理を続けられます。
ただし、async/await自体が新しいスレッドを作るわけではありません。
awaitは、Taskの完了を待ち、完了後に続きの処理を再開する仕組みです。
7-2. Task.Runは別スレッドで処理を実行するための仕組み
Task.Runは、指定した処理をスレッドプール上で実行します。
C#await Task.Run(() =>
{
// 重い計算処理
Calculate();
});
これは、CPUを長時間使う処理をUIスレッドから逃がしたい場合などに使われます。
たとえば、WPFやWindows Formsで重い計算をUIスレッド上で実行すると、画面が固まります。そのような場合にTask.Runを使うことで、UIの応答性を保てます。
7-3. I/Oバウンド処理とCPUバウンド処理の違い
非同期処理を考えるときは、I/Oバウンド処理とCPUバウンド処理を区別することが大切です。
I/Oバウンド処理は、通信、ファイル、データベースなど、外部リソースの応答待ちが中心の処理です。
C#await httpClient.GetStringAsync(url);
await File.ReadAllTextAsync(path);
await dbContext.Users.ToListAsync();
CPUバウンド処理は、計算や変換など、CPUを多く使う処理です。
C#var result = HeavyCalculation();
I/Oバウンド処理では専用の非同期APIを使い、CPUバウンド処理では必要に応じてTask.Runを使う、という考え方が基本です。
7-4. Task.Runを使うべきケース・使わない方がよいケース
Task.Runを使うべきケースは、主にCPU負荷の高い処理を別スレッドで実行したい場合です。
C#var result = await Task.Run(() => HeavyCalculation());
UIアプリで重い処理を実行し、画面が固まるのを避けたい場合に有効です。
一方、Web API呼び出しやDBアクセスのようなI/O処理をTask.Runで囲むのは、基本的にはおすすめできません。
C#// あまりよくない例
var result = await Task.Run(() => httpClient.GetStringAsync(url));
このような場合は、最初から用意されている非同期メソッドを使います。
C#var result = await httpClient.GetStringAsync(url);
Task.Runは「非同期っぽくするための万能ツール」ではなく、「CPU負荷の高い処理を別スレッドに逃がすための手段」と考えましょう。
8. 複数の非同期処理を扱う方法
複数の非同期処理を扱う場合、順番にawaitする方法と、同時に開始してまとめて待つ方法があります。
処理の依存関係があるかどうかによって、適切な書き方が変わります。
8-1. awaitを順番に書いた場合の動き
次のコードでは、非同期処理を順番に実行しています。
C#var result1 = await GetData1Async();
var result2 = await GetData2Async();
var result3 = await GetData3Async();
この場合、GetData1Asyncが終わってからGetData2Asyncが始まり、GetData2Asyncが終わってからGetData3Asyncが始まります。
それぞれの処理に依存関係がある場合は、この書き方で問題ありません。
しかし、3つの処理が互いに独立している場合は、順番に待つと時間がかかります。
8-2. Task.WhenAllで複数処理を並列実行する
互いに独立した非同期処理は、Task.WhenAllでまとめて待てます。
C#Task<string> task1 = GetData1Async();
Task<string> task2 = GetData2Async();
Task<string> task3 = GetData3Async();
string[] results = await Task.WhenAll(task1, task2, task3);
このコードでは、3つの処理を先に開始し、すべて完了するのをTask.WhenAllで待っています。
たとえば、それぞれ3秒かかる処理を順番に実行すると合計9秒かかります。しかし同時に開始すれば、全体として約3秒で完了する可能性があります。
ただし、同時に大量の処理を開始すると、サーバーやDBに負荷をかけることがあります。実務では同時実行数にも注意が必要です。
8-3. Task.WhenAnyで最初に完了した処理を受け取る
Task.WhenAnyを使うと、複数のTaskのうち最初に完了したものを受け取れます。
C#Task<string> task1 = GetData1Async();
Task<string> task2 = GetData2Async();
Task<string> completedTask = await Task.WhenAny(task1, task2);
string result = await completedTask;
Console.WriteLine(result);
Task.WhenAnyは、最初に完了したTaskを返します。
タイムアウト処理や、複数の候補から最初に応答したものを使いたい場合に便利です。
たとえば、次のようにタイムアウトを実装できます。
C#Task<string> dataTask = GetDataAsync();
Task timeoutTask = Task.Delay(3000);
Task completedTask = await Task.WhenAny(dataTask, timeoutTask);
if (completedTask == timeoutTask)
{
Console.WriteLine("タイムアウトしました");
}
else
{
string result = await dataTask;
Console.WriteLine(result);
}
8-4. 複数の非同期処理で例外が発生した場合の扱い
Task.WhenAllで複数の非同期処理を待っている場合、いずれかの処理で例外が発生すると、await Task.WhenAll(...)の時点で例外がスローされます。
C#try
{
await Task.WhenAll(task1, task2, task3);
}
catch (Exception ex)
{
Console.WriteLine($"エラー: {ex.Message}");
}
複数のタスクで例外が発生する可能性がある場合は、各タスクの状態を確認することもあります。
C#Task[] tasks = { task1, task2, task3 };
try
{
await Task.WhenAll(tasks);
}
catch
{
foreach (var task in tasks)
{
if (task.IsFaulted)
{
Console.WriteLine(task.Exception?.GetBaseException().Message);
}
}
}
複数の非同期処理を扱うときは、成功時だけでなく、失敗時の扱いも設計しておくことが大切です。
9. async/awaitを実務で使うときの設計ポイント
実務でasync/awaitを使うときは、単に動くコードを書くだけでなく、保守性、パフォーマンス、例外処理、キャンセル、可読性を意識する必要があります。
ここでは、UIアプリ、ASP.NET Core、DBアクセス、ファイル処理などの場面での設計ポイントを見ていきます。
9-1. UIアプリで画面が固まらないようにする
WPF、Windows Forms、MAUIなどのUIアプリでは、UIスレッドをブロックしないことが重要です。
悪い例は次のようなコードです。
C#private void Button_Click(object sender, EventArgs e)
{
Thread.Sleep(5000);
label.Text = "完了";
}
このコードでは、5秒間画面が固まります。
非同期処理を使うと、画面の応答性を保ちやすくなります。
C#private async void Button_Click(object sender, EventArgs e)
{
await Task.Delay(5000);
label.Text = "完了";
}
イベントハンドラーではasync voidを使うことがありますが、通常のメソッドではasync Taskを使いましょう。
9-2. ASP.NET Coreで非同期処理を使うメリット
ASP.NET Coreでは、DBアクセスや外部API呼び出しを非同期化することで、リクエスト処理中にスレッドを効率よく使えます。
C#[HttpGet]
public async Task<IActionResult> Get()
{
var users = await _dbContext.Users.ToListAsync();
return Ok(users);
}
Webアプリでは、多数のリクエストを同時に処理する必要があります。
同期的にDBや外部APIの応答を待つと、スレッドがブロックされ、スケーラビリティが低下する可能性があります。
非同期処理を使うことで、待機中のスレッドを他の処理に使いやすくなります。
9-3. DBアクセスやファイル処理での使い方
Entity Framework Coreでは、非同期メソッドが多く用意されています。
C#var user = await _dbContext.Users
.FirstOrDefaultAsync(x => x.Id == id);
一覧取得では次のように書けます。
C#var users = await _dbContext.Users.ToListAsync();
ファイル処理でも非同期APIを使えます。
C#string text = await File.ReadAllTextAsync("data.txt");
await File.WriteAllTextAsync("output.txt", text);
DBアクセスやファイル処理では、同期メソッドではなく、可能な限り非同期メソッドを選ぶとよいでしょう。
9-4. 非同期処理を使いすぎるデメリット
async/awaitは便利ですが、何でも非同期にすればよいわけではありません。
短時間で終わる単純な計算処理を無理に非同期化すると、かえってコードが複雑になります。
C#async Task<int> AddAsync(int a, int b)
{
return a + b;
}
このような処理を非同期にする意味はほとんどありません。
また、非同期処理が増えると、例外処理、キャンセル処理、実行順序の管理が複雑になることがあります。
非同期化する価値があるのは、主にI/O待ちが発生する処理や、UIスレッドをブロックしたくない重い処理です。
9-5. 読みやすく保守しやすい非同期コードを書くコツ
保守しやすい非同期コードを書くには、次のような点を意識しましょう。
非同期メソッド名にはAsyncを付けます。
C#GetUserAsync()
SaveOrderAsync()
DownloadFileAsync()
Task.ResultやWait()を避け、awaitで処理をつなげます。
C#var result = await GetDataAsync();
例外処理はtry-catchで明示します。
C#try
{
await SaveAsync();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
キャンセルが必要な処理ではCancellationTokenを受け取れるようにします。
C#async Task SaveAsync(CancellationToken cancellationToken)
{
await Task.Delay(1000, cancellationToken);
}
また、1つのメソッドに多くの非同期処理を詰め込みすぎず、役割ごとに分割することも大切です。
10. C# async/awaitに関するよくある質問
最後に、C#のasync/awaitについて初心者が疑問に思いやすい点をQ&A形式で整理します。
10-1. asyncとawaitは必ずセットで使う必要がある?
基本的には、asyncメソッドの中でawaitを使うことが多いです。
ただし、構文上はasyncだけを付けることもできます。
C#async Task DoWorkAsync()
{
Console.WriteLine("処理");
}
しかし、awaitがないasyncメソッドは、実質的に同期処理のように動きます。コンパイラから警告が出ることもあります。
通常は、awaitする処理がある場合にasyncを付けると考えましょう。
10-2. awaitすると処理は止まる?
awaitすると、そのメソッド内の続きの処理は、対象のTaskが完了するまで待ちます。
C#Console.WriteLine("開始");
await Task.Delay(1000);
Console.WriteLine("終了");
この場合、終了は1秒後に表示されます。
ただし、Thread.Sleepのようにスレッドをブロックし続けるわけではありません。
つまり、「メソッドの続きは待つが、スレッドを無駄に占有しにくい」と理解するとよいです。
10-3. asyncメソッドは別スレッドで動く?
asyncを付けただけでは、別スレッドで動くわけではありません。
C#async Task DoWorkAsync()
{
Console.WriteLine("処理");
}
このメソッドは、呼び出されたスレッドで開始されます。
別スレッドで処理を実行したい場合は、Task.Runなどを使います。
C#await Task.Run(() =>
{
HeavyCalculation();
});
async/awaitはスレッドを作る仕組みではなく、非同期処理を扱いやすくする構文です。
10-4. async voidは絶対に使ってはいけない?
async voidは基本的に避けるべきですが、例外的にイベントハンドラーでは使われます。
C#private async void Button_Click(object sender, EventArgs e)
{
await SaveAsync();
}
イベントハンドラーは戻り値をvoidにする必要があるため、このようなケースではasync voidを使います。
一方、通常の非同期メソッドではTaskを返しましょう。
C#async Task SaveAsync()
{
await Task.Delay(1000);
}
async voidは呼び出し側でawaitできず、例外処理も難しくなるため、イベントハンドラー以外では避けるのが基本です。
10-5. TaskとThreadの違いは?
Threadは実際の実行スレッドを表す低レベルな仕組みです。
一方、Taskは「処理の単位」や「将来完了する処理」を表す高レベルな仕組みです。
Taskは必ずしも新しいスレッドを意味するわけではありません。
たとえば、Task.Delayは待機を表すTaskですが、待機中に専用スレッドが動き続けているわけではありません。
C#await Task.Delay(1000);
一方、Task.Runを使うと、スレッドプール上で処理を実行します。
C#await Task.Run(() => HeavyCalculation());
Taskは非同期処理を扱うための抽象化であり、Threadよりも高いレベルで処理を管理するものだと考えるとよいでしょう。
10-6. awaitを書かないとコンパイルエラーになる?
awaitを書かなくても、必ずコンパイルエラーになるわけではありません。
C#DoWorkAsync();
このように非同期メソッドを呼び出すだけなら、コンパイルできる場合があります。
ただし、処理の完了を待たずに次へ進むため、意図しない動作になることがあります。また、コンパイラや解析ツールから警告が出ることもあります。
戻り値が必要な場合は、awaitを書かないと期待する値を取得できません。
C#Task<string> task = GetMessageAsync();
この時点ではstringではなくTask<string>です。
結果を取得するには次のようにします。
C#string message = await GetMessageAsync();
非同期処理を確実に待ちたい場合は、awaitを書きましょう。
まとめ
C#のasync/awaitは、非同期処理をわかりやすく、安全に書くための重要な仕組みです。
asyncはメソッド内でawaitを使えるようにするキーワードで、awaitはTaskの完了を待ってから続きの処理を再開するためのキーワードです。
非同期メソッドでは、戻り値がない場合はTask、戻り値がある場合はTask<T>を使うのが基本です。async voidはイベントハンドラーを除き、できるだけ避けましょう。
また、async/awaitは別スレッドで処理を実行するための仕組みではありません。別スレッドでCPU負荷の高い処理を実行したい場合は、Task.Runを検討します。一方、Web API、DBアクセス、ファイル処理などのI/O処理では、専用の非同期APIをawaitするのが基本です。
初心者が特につまずきやすいのは、awaitの書き忘れ、asyncを付けただけで非同期になるという誤解、Task.ResultやWait()の乱用、async voidの使い方です。
まずは次のポイントを押さえておきましょう。
C#async Task DoWorkAsync()
{
await Task.Delay(1000);
}
C#string result = await GetDataAsync();
C#await Task.WhenAll(task1, task2, task3);
async/awaitを正しく理解すると、UIが固まらないアプリ、効率よくリクエストを処理できるWebアプリ、読みやすく保守しやすい非同期コードを書けるようになります。C#で実務的な開発を行ううえで、async/awaitは必ず身につけておきたい重要な知識です。

