C# async/awaitの使い方を基礎から解説|非同期処理の仕組み・Task・よくあるエラーまでわかる入門ガイド

はじめに

C#でアプリケーションを開発していると、ファイル読み込み、HTTP通信、データベースアクセス、外部API呼び出しなど、処理に時間がかかる場面がよくあります。こうした処理を同期的に実行すると、処理が終わるまで画面が固まったり、Webアプリの応答が遅くなったりする原因になります。

そこで重要になるのが、C#のasync/awaitを使った非同期処理です。

async/awaitを使うと、時間のかかる処理の完了を待ちながらも、呼び出し元のスレッドをブロックせずに処理を進められます。特に、HTTP通信やファイルI/OのようなI/Oバウンド処理では、アプリケーションの応答性やスケーラビリティを大きく改善できます。

この記事では、C# async/awaitの基礎から、Taskとの関係、実践的なコード例、よくあるエラー、例外処理、キャンセル処理、ベストプラクティスまでを入門者向けにわかりやすく解説します。

1. C#のasync/awaitとは?非同期処理を基礎から理解しよう

C#のasync/awaitは、非同期処理をわかりやすく書くための構文です。

非同期処理とは、時間のかかる処理の完了を待っている間に、呼び出し元の処理をブロックしない実行方式です。たとえば、Web APIからデータを取得する処理では、サーバーからの応答を待つ時間が発生します。この待ち時間にスレッドを占有し続けるのではなく、他の処理にスレッドを使えるようにするのが非同期処理の大きな目的です。

C#では、主にTaskTask<T>と組み合わせてasync/awaitを使います。

C#
public async Task<string> GetMessageAsync()
{
await Task.Delay(1000);
return "Hello async!";
}

このコードでは、Task.Delay(1000)によって1秒待機しますが、Thread.Sleepのようにスレッドをブロックしません。awaitを使うことで、非同期処理の完了を自然な形で待てます。

1-1. async/awaitでできること

async/awaitを使うと、時間のかかる処理をシンプルなコードで非同期化できます。

たとえば、次のような処理に向いています。

ファイルの読み書き、HTTP通信、データベースアクセス、クラウドサービスとの通信、外部APIの呼び出し、一定時間の待機処理などです。

従来のコールバック形式やイベント形式の非同期処理では、処理の流れが複雑になりやすい問題がありました。しかし、async/awaitを使うと、見た目は同期処理に近い形で非同期処理を書けます。

C#
public async Task ShowDataAsync()
{
string data = await GetDataAsync();
Console.WriteLine(data);
}

このように、awaitの後に処理を続けて書けるため、読みやすく、保守しやすいコードになります。

1-2. 非同期処理が必要になる場面

非同期処理が特に必要になるのは、「待ち時間」が発生する処理です。

代表的なのは、HTTP通信です。

C#
using HttpClient client = new HttpClient();
string html = await client.GetStringAsync("https://example.com");

WebサイトやAPIからレスポンスが返ってくるまでには時間がかかります。この間、同期処理で待ってしまうと、アプリケーション全体の応答性が低下します。

デスクトップアプリでは、同期処理で長時間待つとUIが固まります。Webアプリでは、リクエストを処理するスレッドが無駄に占有され、同時に処理できるリクエスト数が減ってしまいます。

そのため、時間のかかるI/O処理では、C# async/awaitを使って非同期化するのが一般的です。

1-3. 同期処理との違い

同期処理は、1つの処理が終わるまで次の処理に進みません。

C#
Console.WriteLine("開始");
Thread.Sleep(3000);
Console.WriteLine("終了");

このコードでは、Thread.Sleep(3000)の間、現在のスレッドが完全に停止します。

一方、非同期処理では、待機中にスレッドをブロックしません。

C#
Console.WriteLine("開始");
await Task.Delay(3000);
Console.WriteLine("終了");

Task.Delayは3秒待ちますが、その間スレッドを占有し続けるわけではありません。処理の完了後、続きの処理が再開されます。

同期処理は単純でわかりやすい一方、待ち時間が多い処理では効率が悪くなります。非同期処理は、待ち時間を有効活用できるため、応答性や効率を高められます。

1-4. async/awaitは「別スレッドで実行する仕組み」なのか?

async/awaitを使うと、必ず別スレッドで処理が実行されると思われがちですが、これは正確ではありません。

async/awaitは、基本的には「非同期処理の完了を待つための仕組み」です。必ず新しいスレッドを作るわけではありません。

たとえば、HTTP通信やファイルI/OなどのI/Oバウンド処理では、待機中にスレッドを解放できます。別スレッドでずっと処理しているのではなく、OSやランタイムの非同期I/O機能を利用して、完了したら続きを実行するイメージです。

一方、CPU負荷の高い計算処理をバックグラウンドで実行したい場合は、Task.Runを使ってスレッドプール上で実行することがあります。

C#
int result = await Task.Run(() =>
{
return HeavyCalculation();
});

つまり、async/awaitそのものは「別スレッドを作る構文」ではありません。非同期処理を扱いやすくするための構文だと理解することが重要です。

2. C#の非同期処理の仕組み

C#の非同期処理を理解するには、asyncawaitTaskの関係を押さえる必要があります。

asyncを付けたメソッドは、内部でawaitを使えるようになります。そして、awaitTaskなどの非同期処理が完了するまで、メソッドの続きを一時的に中断します。

ただし、中断するといってもスレッドをブロックするわけではありません。ここがThread.SleepTask.Waitとの大きな違いです。

2-1. 非同期処理の基本的な流れ

非同期処理の基本的な流れは次のようになります。

まず、非同期メソッドが呼び出されます。次に、メソッド内でawaitに到達します。await対象の処理がまだ完了していなければ、そのメソッドはいったん呼び出し元に制御を返します。そして、非同期処理が完了したタイミングで、awaitの次の行から処理が再開されます。

C#
public async Task SampleAsync()
{
Console.WriteLine("1. 開始");

await Task.Delay(1000);

Console.WriteLine("2. 再開");
}

この例では、Task.Delay(1000)の完了を待ってから、Console.WriteLine("2. 再開")が実行されます。

ポイントは、待機中にスレッドを止めているわけではないことです。awaitは「処理が終わるまでブロックする」のではなく、「処理が終わったら続きを実行する」という動きになります。

2-2. awaitで処理が一時停止・再開される仕組み

awaitに到達すると、C#コンパイラはそのメソッドを状態マシンのような形に変換します。

開発者は通常、この内部変換を意識する必要はありません。しかし、仕組みをざっくり理解しておくと、async/awaitの挙動がわかりやすくなります。

C#
public async Task<string> LoadAsync()
{
string text = await File.ReadAllTextAsync("sample.txt");
return text;
}

このメソッドでは、File.ReadAllTextAsyncが完了するまで待ちます。未完了の場合、メソッドはいったん中断され、呼び出し元にTask<string>を返します。

ファイル読み込みが完了すると、awaitの次の処理、つまりreturn text;が実行されます。

このように、awaitは処理を分割し、「非同期処理の前」と「完了後の続き」を自然につないでくれます。

2-3. スレッドと非同期処理の違い

スレッドは、実際にコードを実行する単位です。一方、非同期処理は、処理の完了を待つ間にスレッドを効率よく使うための仕組みです。

たとえば、同期的なI/O処理では、ファイル読み込みやネットワーク応答を待っている間もスレッドが占有されます。

C#
string text = File.ReadAllText("sample.txt");

一方、非同期I/Oでは、待機中にスレッドを解放できます。

C#
string text = await File.ReadAllTextAsync("sample.txt");

つまり、非同期処理は「スレッドを増やす」ためのものではなく、「スレッドを無駄に待たせない」ためのものです。

もちろん、Task.Runを使えば別スレッドで処理を実行できます。しかし、HTTP通信やファイルI/Oのような処理では、単純にTask.Runで包むのではなく、もともと用意されている非同期APIを使うのが基本です。

2-4. I/Oバウンド処理とCPUバウンド処理の違い

非同期処理を理解するうえで重要なのが、I/Oバウンド処理とCPUバウンド処理の違いです。

I/Oバウンド処理とは、主に外部の応答待ちがボトルネックになる処理です。HTTP通信、ファイルアクセス、データベースアクセスなどが該当します。このような処理では、async/awaitを使うメリットが大きくなります。

C#
string json = await httpClient.GetStringAsync(url);

CPUバウンド処理とは、CPUの計算能力がボトルネックになる処理です。大量の計算、画像処理、暗号化、圧縮処理などが該当します。

CPUバウンド処理をUIスレッドで実行すると画面が固まるため、必要に応じてTask.Runでバックグラウンド実行します。

C#
int result = await Task.Run(() => Calculate());

I/Oバウンド処理では非同期APIを使い、CPUバウンド処理では必要に応じてTask.Runを使う、という使い分けが基本です。

3. async/awaitの基本構文と使い方

C#でasync/awaitを使うには、非同期メソッドにasync修飾子を付け、非同期処理の完了を待つ場所でawaitを使います。

基本形は次のとおりです。

C#
public async Task MethodNameAsync()
{
await SomeAsyncMethod();
}

戻り値がある場合は、Task<T>を使います。

C#
public async Task<int> GetNumberAsync()
{
await Task.Delay(1000);
return 100;
}

3-1. asyncメソッドの書き方

asyncメソッドは、メソッド宣言にasyncを付けて定義します。

C#
public async Task DoWorkAsync()
{
await Task.Delay(1000);
Console.WriteLine("完了しました");
}

asyncを付けることで、メソッド内でawaitを使えるようになります。

ただし、asyncを付けただけで自動的に非同期になるわけではありません。メソッド内でawaitを使って非同期処理を待つことで、非同期メソッドとして意味を持ちます。

悪い例は次のようなコードです。

C#
public async Task DoWorkAsync()
{
Console.WriteLine("同期的な処理だけです");
}

このコードはasyncが付いていますが、awaitがありません。コンパイルはできますが、警告が出ます。非同期処理がないなら、asyncを付ける必要はありません。

3-2. awaitの使い方

awaitは、TaskTask<T>などの完了を待つために使います。

C#
await Task.Delay(1000);

戻り値がある非同期処理では、awaitの結果を変数に代入できます。

C#
string result = await GetTextAsync();
Console.WriteLine(result);

awaitを使うと、その非同期処理が完了するまで、以降の処理は実行されません。ただし、スレッドをブロックせずに待機します。

C#
public async Task PrintAsync()
{
Console.WriteLine("開始");

string message = await GetMessageAsync();

Console.WriteLine(message);
Console.WriteLine("終了");
}

このように、awaitを使うことで、非同期処理であっても上から下へ読みやすいコードを書けます。

3-3. 戻り値がない非同期メソッド

戻り値がない非同期メソッドは、基本的にTaskを返します。

C#
public async Task SaveAsync()
{
await File.WriteAllTextAsync("sample.txt", "Hello");
}

ここで注意したいのは、戻り値がないからといってvoidにしないことです。

C#
public async void SaveAsync()
{
await File.WriteAllTextAsync("sample.txt", "Hello");
}

このようなasync voidは、例外処理や呼び出し元での待機が難しくなるため、通常のメソッドでは避けるべきです。

async voidを使うのは、主にイベントハンドラーなど、戻り値をvoidにする必要がある場面に限定します。

C#
private async void Button_Click(object sender, EventArgs e)
{
await SaveAsync();
}

3-4. 戻り値がある非同期メソッド

戻り値がある非同期メソッドでは、Task<T>を使います。

C#
public async Task<string> ReadTextAsync()
{
string text = await File.ReadAllTextAsync("sample.txt");
return text;
}

呼び出し側では、awaitを使って結果を受け取ります。

C#
string text = await ReadTextAsync();
Console.WriteLine(text);

Task<T>Tには、戻り値の型を指定します。

C#
public async Task<int> GetCountAsync()
{
await Task.Delay(500);
return 10;
}

この場合、await GetCountAsync()の結果はintになります。

C#
int count = await GetCountAsync();

3-5. Mainメソッドでasync/awaitを使う方法

C#では、コンソールアプリのMainメソッドでもasync/awaitを使えます。

C#
class Program
{
static async Task Main(string[] args)
{
await RunAsync();
}

static async Task RunAsync()
{
await Task.Delay(1000);
Console.WriteLine("完了");
}
}

Mainメソッドの戻り値をTaskまたはTask<int>にすることで、awaitを直接使えます。

C#
static async Task<int> Main(string[] args)
{
await Task.Delay(1000);
return 0;
}

古い書き方では、Mainから非同期メソッドを呼び出してWait()する例もありますが、デッドロックや例外処理の問題を避けるため、可能であればasync Task Mainを使うのが自然です。

4. Taskとは?async/awaitとTaskの関係

C#の非同期処理を理解するには、Taskの理解が欠かせません。

Taskは、非同期処理の状態や結果を表す型です。処理が完了したか、失敗したか、キャンセルされたかといった情報を持っています。

asyncメソッドは、通常TaskまたはTask<T>を返します。そして、awaitはそのTaskの完了を待ちます。

4-1. Taskの役割

Taskは、「将来完了する処理」を表すオブジェクトです。

たとえば、次のメソッドは、1秒後に完了する処理を表すTaskを返します。

C#
public Task WaitAsync()
{
return Task.Delay(1000);
}

このメソッド自体にはasyncが付いていませんが、Taskを返しているため非同期処理として扱えます。

呼び出し側では、次のようにawaitできます。

C#
await WaitAsync();

Taskは処理の完了状態を管理します。完了すれば成功、例外が発生すれば失敗、キャンセルされればキャンセル状態になります。

4-2. TaskとTask<T>の違い

TaskTask<T>の違いは、戻り値があるかどうかです。

Taskは戻り値のない非同期処理を表します。

C#
public async Task SaveAsync()
{
await File.WriteAllTextAsync("sample.txt", "Hello");
}

Task<T>は戻り値のある非同期処理を表します。

C#
public async Task<string> LoadAsync()
{
return await File.ReadAllTextAsync("sample.txt");
}

呼び出し側では、Task<T>awaitするとT型の値を取得できます。

C#
string text = await LoadAsync();

つまり、同期メソッドでいうvoidに近いものがTask、戻り値ありの型に近いものがTask<T>です。

4-3. Taskを返すメソッドの作り方

非同期メソッドを作るときは、基本的にasync Taskまたはasync Task<T>を使います。

C#
public async Task DownloadAsync()
{
using HttpClient client = new HttpClient();
string content = await client.GetStringAsync("https://example.com");

await File.WriteAllTextAsync("page.html", content);
}

戻り値がある場合は次のように書きます。

C#
public async Task<string> DownloadTextAsync(string url)
{
using HttpClient client = new HttpClient();
return await client.GetStringAsync(url);
}

ただし、単純に別のTaskをそのまま返すだけなら、asyncawaitを省略できる場合もあります。

C#
public Task<string> DownloadTextAsync(string url)
{
using HttpClient client = new HttpClient();
return client.GetStringAsync(url);
}

ただし、この例ではHttpClientがすぐに破棄されるため、実際には適切ではありません。リソース管理や例外処理が必要な場合は、async/awaitを使って明示的に書く方が安全です。

4-4. Task.Runの使いどころ

Task.Runは、処理をスレッドプール上で実行するためのメソッドです。

C#
int result = await Task.Run(() =>
{
return HeavyCalculation();
});

主にCPU負荷の高い処理を、UIスレッドから切り離したい場合に使います。

たとえば、WPFやWindows Formsで重い計算処理を直接実行すると、画面が固まります。このような場合、Task.Runでバックグラウンド実行することで、UIの応答性を保てます。

一方、HTTP通信やデータベースアクセスなど、もともと非同期APIが用意されている処理をTask.Runで包む必要は基本的にありません。

悪い例です。

C#
string result = await Task.Run(() =>
{
return httpClient.GetStringAsync(url).Result;
});

このような書き方は、非同期APIを同期的に待ってから別スレッドで動かしているだけで、メリットが少なく、トラブルの原因にもなります。

良い例は次のように、非同期APIをそのままawaitすることです。

C#
string result = await httpClient.GetStringAsync(url);

4-5. Task.DelayとThread.Sleepの違い

Task.DelayThread.Sleepは、どちらも一定時間待つ処理に見えますが、動きは大きく異なります。

Thread.Sleepは、現在のスレッドを指定時間だけ停止させます。

C#
Thread.Sleep(1000);

この間、スレッドは何もできません。UIスレッドで使うと画面が固まります。

一方、Task.Delayは非同期的な待機を表します。

C#
await Task.Delay(1000);

Task.Delayは、待機中にスレッドをブロックしません。そのため、非同期メソッド内で待機したい場合は、Thread.SleepではなくTask.Delayを使うのが基本です。

特に、次のようなコードは避けましょう。

C#
public async Task BadAsync()
{
Thread.Sleep(1000);
}

非同期メソッド内では、次のように書きます。

C#
public async Task GoodAsync()
{
await Task.Delay(1000);
}

5. async/awaitの実践例

ここからは、C# async/awaitの実践的な使い方を見ていきます。

非同期処理は、実際のコードを通して理解するのが一番です。ファイル読み込み、HTTP通信、複数処理の順次実行・並列実行、Task.WhenAllTask.WhenAnyの使い方を確認しましょう。

5-1. ファイル読み込みを非同期で行う例

ファイルを非同期で読み込むには、File.ReadAllTextAsyncを使います。

C#
public async Task<string> ReadFileAsync(string path)
{
string text = await File.ReadAllTextAsync(path);
return text;
}

呼び出し側は次のように書きます。

C#
string content = await ReadFileAsync("sample.txt");
Console.WriteLine(content);

ファイルの書き込みも非同期で行えます。

C#
public async Task WriteFileAsync(string path, string content)
{
await File.WriteAllTextAsync(path, content);
}

大きなファイルを扱う場合、同期的に読み書きすると処理中にスレッドがブロックされます。非同期APIを使うことで、アプリケーションの応答性を保ちやすくなります。

5-2. HTTP通信を非同期で行う例

HTTP通信では、HttpClientの非同期メソッドを使います。

C#
using System.Net.Http;

public async Task<string> GetHtmlAsync(string url)
{
using HttpClient client = new HttpClient();
string html = await client.GetStringAsync(url);
return html;
}

呼び出し側は次のようになります。

C#
string html = await GetHtmlAsync("https://example.com");
Console.WriteLine(html);

実際のアプリケーションでは、HttpClientを毎回newするのではなく、再利用する設計が推奨されることが多いです。たとえば、クラスのフィールドとして持つ方法があります。

C#
private static readonly HttpClient httpClient = new HttpClient();

public async Task<string> GetHtmlAsync(string url)
{
return await httpClient.GetStringAsync(url);
}

HTTP通信は応答待ちが発生する典型的なI/Oバウンド処理なので、async/awaitとの相性が非常に良い処理です。

5-3. 複数の非同期処理を順番に実行する例

複数の非同期処理を順番に実行したい場合は、awaitを順番に書きます。

C#
public async Task ExecuteSequentiallyAsync()
{
string user = await GetUserAsync();
string orders = await GetOrdersAsync(user);
string report = await CreateReportAsync(orders);

Console.WriteLine(report);
}

この場合、GetUserAsyncが完了してからGetOrdersAsyncが実行され、さらにその後にCreateReportAsyncが実行されます。

処理の結果が次の処理に必要な場合は、順番にawaitする必要があります。

C#
public async Task<string> GetUserAsync()
{
await Task.Delay(500);
return "User001";
}

public async Task<string> GetOrdersAsync(string user)
{
await Task.Delay(500);
return $"{user}の注文一覧";
}

public async Task<string> CreateReportAsync(string orders)
{
await Task.Delay(500);
return $"レポート: {orders}";
}

依存関係がある処理は順番に実行し、依存関係がない処理は並列化を検討するのが基本です。

5-4. 複数の非同期処理を並列に実行する例

互いに依存しない非同期処理は、並列に開始してからまとめて待つことで、全体の処理時間を短縮できます。

悪い例は、独立した処理を順番に待つ書き方です。

C#
string a = await GetDataAAsync();
string b = await GetDataBAsync();
string c = await GetDataCAsync();

この場合、A、B、Cが順番に実行されます。

並列に実行したい場合は、まずTaskを開始し、その後でawaitします。

C#
Task<string> taskA = GetDataAAsync();
Task<string> taskB = GetDataBAsync();
Task<string> taskC = GetDataCAsync();

string a = await taskA;
string b = await taskB;
string c = await taskC;

さらに、Task.WhenAllを使うとまとめて待てます。

C#
Task<string> taskA = GetDataAAsync();
Task<string> taskB = GetDataBAsync();
Task<string> taskC = GetDataCAsync();

string[] results = await Task.WhenAll(taskA, taskB, taskC);

foreach (string result in results)
{
Console.WriteLine(result);
}

このように、独立した非同期処理では、順番に待つのではなく、先に開始してからまとめて待つのが効率的です。

5-5. Task.WhenAllとTask.WhenAnyの使い方

Task.WhenAllは、複数のTaskがすべて完了するまで待つメソッドです。

C#
public async Task RunAllAsync()
{
Task task1 = Task.Delay(1000);
Task task2 = Task.Delay(2000);
Task task3 = Task.Delay(3000);

await Task.WhenAll(task1, task2, task3);

Console.WriteLine("すべて完了しました");
}

戻り値があるTask<T>にも使えます。

C#
Task<string> task1 = GetDataAsync("A");
Task<string> task2 = GetDataAsync("B");

string[] results = await Task.WhenAll(task1, task2);

一方、Task.WhenAnyは、複数のTaskのうち、どれか1つが完了するまで待ちます。

C#
public async Task RunAnyAsync()
{
Task<string> task1 = GetDataAsync("A");
Task<string> task2 = GetDataAsync("B");

Task<string> completedTask = await Task.WhenAny(task1, task2);

string result = await completedTask;
Console.WriteLine($"最初に完了: {result}");
}

Task.WhenAnyは、複数のサーバーにリクエストを送り、最初に応答したものを使う場合や、タイムアウト処理を実装する場合に便利です。

6. async/awaitでよくあるエラーと原因

async/awaitは便利ですが、初心者がつまずきやすいポイントもあります。

特に多いのは、awaitの付け忘れ、async voidの使用、Task.ResultWait()によるデッドロック、例外処理の誤解、コンパイルエラーです。

ここでは、よくあるエラーと原因を整理します。

6-1. awaitを付け忘れる

非同期メソッドを呼び出したのにawaitを付け忘れると、処理の完了を待たずに次の行へ進んでしまいます。

C#
public async Task SampleAsync()
{
SaveAsync();
Console.WriteLine("保存完了");
}

このコードでは、SaveAsync()の完了を待たずにConsole.WriteLineが実行される可能性があります。

正しくは次のように書きます。

C#
public async Task SampleAsync()
{
await SaveAsync();
Console.WriteLine("保存完了");
}

戻り値がある場合も同様です。

C#
Task<string> task = GetMessageAsync();

この時点では、taskは処理結果ではなく、非同期処理を表すTask<string>です。

結果を取得するにはawaitします。

C#
string message = await GetMessageAsync();

6-2. asyncメソッドの戻り値をvoidにしてしまう

戻り値がない非同期メソッドをasync voidで書いてしまうのは、よくあるミスです。

C#
public async void SaveAsync()
{
await File.WriteAllTextAsync("sample.txt", "Hello");
}

async voidは呼び出し側でawaitできません。

C#
await SaveAsync(); // コンパイルできない

また、例外が呼び出し元に伝わりにくく、テストもしづらくなります。

通常はTaskを返します。

C#
public async Task SaveAsync()
{
await File.WriteAllTextAsync("sample.txt", "Hello");
}

例外的に、イベントハンドラーではasync voidを使います。

C#
private async void Button_Click(object sender, EventArgs e)
{
await SaveAsync();
}

この場合も、イベントハンドラー内で適切に例外処理を行うのが安全です。

6-3. Task.ResultやWaitでデッドロックする

非同期処理を同期的に待つために、Task.ResultWait()を使うと、デッドロックや応答停止の原因になることがあります。

C#
string result = GetDataAsync().Result;
C#
GetDataAsync().Wait();

特に、UIアプリや古いASP.NET環境では問題が起きやすいです。awaitで非同期的に待つのが基本です。

C#
string result = await GetDataAsync();

デッドロックの原因は、非同期処理の続きが元のコンテキストに戻ろうとしているのに、そのスレッドが.Result.Wait()でブロックされている状態になることです。

非同期メソッドを使うなら、呼び出し元も含めてasync/awaitでつなげるのが理想です。

C#
public async Task ControllerActionAsync()
{
string result = await GetDataAsync();
}

6-4. 例外がcatchできない

非同期処理の例外は、awaitしたタイミングで再スローされます。

C#
try
{
await ThrowExceptionAsync();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}

このように書けば、通常は例外をキャッチできます。

しかし、awaitせずに呼び出すと、例外を適切に捕捉できない場合があります。

C#
try
{
ThrowExceptionAsync();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}

このコードでは、ThrowExceptionAsyncの完了を待っていないため、非同期処理内で発生した例外をこのcatchで捕捉できません。

正しくは次のようにします。

C#
try
{
await ThrowExceptionAsync();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}

また、async voidで発生した例外も扱いが難しくなります。通常の非同期メソッドではTaskを返すようにしましょう。

6-5. CS4032などasync/await関連のコンパイルエラー

C# async/awaitでは、いくつか代表的なコンパイルエラーがあります。

たとえば、awaitasyncではないメソッド内で使うとエラーになります。

C#
public Task SampleAsync()
{
await Task.Delay(1000); // エラー
}

awaitを使うメソッドにはasyncを付けます。

C#
public async Task SampleAsync()
{
await Task.Delay(1000);
}

また、Mainメソッドでawaitを使いたい場合も、async Task Mainにする必要があります。

C#
static async Task Main(string[] args)
{
await Task.Delay(1000);
}

ほかにも、awaitできない型に対してawaitを使うとエラーになります。

C#
int number = 10;
await number; // エラー

awaitできるのは、基本的にTaskTask<T>ValueTaskValueTask<T>など、await可能な型です。

コンパイルエラーが出た場合は、「そのメソッドにasyncが付いているか」「戻り値がTaskまたはTask<T>になっているか」「await対象が非同期処理か」を確認しましょう。

7. async/awaitの例外処理・キャンセル処理

実際のアプリケーションでは、非同期処理が必ず成功するとは限りません。HTTP通信が失敗する、ファイルが存在しない、処理がタイムアウトする、ユーザーがキャンセルする、といったケースがあります。

そのため、async/awaitを使うときは、例外処理とキャンセル処理もセットで理解しておくことが大切です。

7-1. try-catchで例外を処理する方法

非同期メソッドの例外は、awaitした場所で通常の例外と同じようにtry-catchできます。

C#
public async Task LoadAsync()
{
try
{
string text = await File.ReadAllTextAsync("notfound.txt");
Console.WriteLine(text);
}
catch (FileNotFoundException ex)
{
Console.WriteLine($"ファイルが見つかりません: {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"予期しないエラー: {ex.Message}");
}
}

HTTP通信でも同様です。

C#
public async Task GetDataAsync()
{
try
{
using HttpClient client = new HttpClient();
string result = await client.GetStringAsync("https://example.com/api");
Console.WriteLine(result);
}
catch (HttpRequestException ex)
{
Console.WriteLine($"通信エラー: {ex.Message}");
}
}

awaitを使っていれば、非同期処理内で発生した例外も自然に扱えます。

7-2. Task内で発生した例外の扱い

Task内で例外が発生すると、そのTaskは失敗状態になります。そして、awaitしたタイミングで例外がスローされます。

C#
public async Task ThrowAsync()
{
await Task.Delay(500);
throw new InvalidOperationException("エラーが発生しました");
}

呼び出し側は次のように処理できます。

C#
try
{
await ThrowAsync();
}
catch (InvalidOperationException ex)
{
Console.WriteLine(ex.Message);
}

複数のTaskTask.WhenAllで待つ場合、いずれかのTaskで例外が発生すると、await Task.WhenAll(...)のタイミングで例外がスローされます。

C#
try
{
await Task.WhenAll(Task1Async(), Task2Async(), Task3Async());
}
catch (Exception ex)
{
Console.WriteLine($"エラー: {ex.Message}");
}

複数の例外を詳しく扱いたい場合は、各Taskの状態を確認する設計が必要になることもあります。まずは、awaitした場所で例外を捕捉する、という基本を押さえましょう。

7-3. CancellationTokenで処理をキャンセルする方法

非同期処理をキャンセルできるようにするには、CancellationTokenを使います。

C#
public async Task DoWorkAsync(CancellationToken cancellationToken)
{
for (int i = 0; i < 10; i++)
{
cancellationToken.ThrowIfCancellationRequested();

Console.WriteLine($"処理中: {i}");
await Task.Delay(1000, cancellationToken);
}
}

呼び出し側では、CancellationTokenSourceを作成します。

C#
using CancellationTokenSource cts = new CancellationTokenSource();

Task task = DoWorkAsync(cts.Token);

// 何らかの条件でキャンセル
cts.Cancel();

try
{
await task;
}
catch (OperationCanceledException)
{
Console.WriteLine("処理がキャンセルされました");
}

CancellationTokenは、キャンセル要求を伝えるための仕組みです。強制的にスレッドを停止するものではありません。処理側がトークンを確認し、適切なタイミングで終了する必要があります。

HTTP通信でも、キャンセルトークンを渡せます。

C#
using CancellationTokenSource cts = new CancellationTokenSource();

using HttpClient client = new HttpClient();
string result = await client.GetStringAsync("https://example.com", cts.Token);

7-4. タイムアウト処理の実装方法

非同期処理にタイムアウトを設けたい場合も、CancellationTokenSourceを使えます。

C#
public async Task<string> GetWithTimeoutAsync(string url)
{
using CancellationTokenSource cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(5));

using HttpClient client = new HttpClient();

try
{
return await client.GetStringAsync(url, cts.Token);
}
catch (OperationCanceledException)
{
return "タイムアウトしました";
}
}

CancelAfterを使うと、指定時間が経過した時点でキャンセル要求が発行されます。

また、Task.WhenAnyを使ってタイムアウトを表現する方法もあります。

C#
public async Task<string> GetWithTimeoutAsync(Task<string> task, TimeSpan timeout)
{
Task delayTask = Task.Delay(timeout);

Task completedTask = await Task.WhenAny(task, delayTask);

if (completedTask == delayTask)
{
return "タイムアウトしました";
}

return await task;
}

ただし、Task.WhenAnyだけでは元の処理をキャンセルするわけではありません。実際に処理を止めたい場合は、CancellationTokenと組み合わせるのが安全です。

8. async/awaitを使うときの注意点とベストプラクティス

C# async/awaitを正しく使うには、いくつかの定番ルールを押さえておくと安心です。

特に重要なのは、async voidを避けること、.Result.Wait()を使わないこと、Task.Runを乱用しないこと、非同期メソッド名にAsyncを付けることです。

8-1. async voidは原則使わない

async voidは原則として使わないようにしましょう。

悪い例です。

C#
public async void LoadAsync()
{
await Task.Delay(1000);
}

このメソッドは呼び出し側でawaitできません。完了を待てず、例外も扱いにくくなります。

良い例です。

C#
public async Task LoadAsync()
{
await Task.Delay(1000);
}

Taskを返せば、呼び出し側でawaitできます。

C#
await LoadAsync();

ただし、イベントハンドラーは戻り値がvoidである必要があるため、例外的にasync voidを使います。

C#
private async void Button_Click(object sender, EventArgs e)
{
try
{
await LoadAsync();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}

イベントハンドラーでasync voidを使う場合も、内部でtry-catchするなど、例外処理を意識しましょう。

8-2. ConfigureAwait(false)はいつ使うべきか

ConfigureAwait(false)は、await後に元の同期コンテキストへ戻らなくてもよい場合に使います。

C#
string text = await File.ReadAllTextAsync("sample.txt").ConfigureAwait(false);

ライブラリコードでは、呼び出し元のUIスレッドや同期コンテキストに戻る必要がないことが多いため、ConfigureAwait(false)を使うことがあります。

一方、UIアプリでawait後に画面部品を更新する場合は、元のUIスレッドに戻る必要があります。

C#
private async void Button_Click(object sender, EventArgs e)
{
string text = await LoadTextAsync();

label.Text = text;
}

このような場合にむやみにConfigureAwait(false)を使うと、UI部品を別スレッドから操作してエラーになる可能性があります。

アプリケーションコードでは無理に使わなくてもよい場面が多く、ライブラリコードでは検討する、という理解から始めるとよいでしょう。

8-3. Task.Runを乱用しない

Task.Runは便利ですが、何でもTask.Runで包めばよいわけではありません。

悪い例です。

C#
string result = await Task.Run(() =>
{
return File.ReadAllText("sample.txt");
});

このコードは、同期的なファイル読み込みを別スレッドで実行しているだけです。ファイルI/Oには非同期APIが用意されているため、次のように書く方が自然です。

C#
string result = await File.ReadAllTextAsync("sample.txt");

HTTP通信も同じです。

C#
string result = await httpClient.GetStringAsync(url);

Task.Runは主にCPUバウンド処理をバックグラウンドに逃がしたい場合に使います。

C#
int result = await Task.Run(() => HeavyCalculation());

I/Oバウンド処理では非同期APIを使い、CPUバウンド処理では必要に応じてTask.Runを使う、という使い分けを意識しましょう。

8-4. 非同期メソッド名にはAsyncを付ける

C#では、非同期メソッド名の末尾にAsyncを付けるのが一般的です。

C#
public async Task SaveAsync()
{
await File.WriteAllTextAsync("sample.txt", "Hello");
}

戻り値がある場合も同様です。

C#
public async Task<string> LoadAsync()
{
return await File.ReadAllTextAsync("sample.txt");
}

Asyncを付けることで、そのメソッドが非同期処理であることが呼び出し側からわかりやすくなります。

C#
string text = await LoadAsync();

逆に、同期メソッドにはAsyncを付けません。

C#
public string Load()
{
return File.ReadAllText("sample.txt");
}

メソッド名のルールをそろえることで、コードの可読性が高まり、awaitの付け忘れにも気づきやすくなります。

8-5. UIアプリとWebアプリでの注意点

UIアプリとWebアプリでは、async/awaitを使う目的や注意点が少し異なります。

WPF、Windows Forms、MAUIなどのUIアプリでは、UIスレッドをブロックしないことが重要です。重い処理や待ち時間のある処理を同期的に実行すると、画面が固まります。

C#
private async void Button_Click(object sender, EventArgs e)
{
button.Enabled = false;

try
{
string result = await LoadDataAsync();
label.Text = result;
}
finally
{
button.Enabled = true;
}
}

このように、イベントハンドラーから非同期メソッドをawaitすることで、UIの応答性を保てます。

一方、ASP.NET CoreなどのWebアプリでは、リクエスト処理中にスレッドをブロックしないことが重要です。データベースアクセスやHTTP通信を非同期化することで、サーバーのスレッドを効率よく使えます。

C#
public async Task<IActionResult> Index()
{
var items = await repository.GetItemsAsync();
return View(items);
}

Webアプリでは、.Result.Wait()で非同期処理を同期的に待たないことが特に重要です。基本的には、コントローラーやサービス層までasync/awaitでつなげて書きます。

9. async/awaitの理解を深めるFAQ

ここでは、C# async/awaitを学び始めた人が疑問に感じやすいポイントをFAQ形式で整理します。

9-1. asyncを付けるだけで非同期になる?

asyncを付けるだけでは、実質的に非同期処理になるとは限りません。

C#
public async Task SampleAsync()
{
Console.WriteLine("Hello");
}

このようにawaitがない場合、処理は同期的に実行されます。コンパイラから警告も出ます。

非同期処理にするには、awaitできる処理を呼び出す必要があります。

C#
public async Task SampleAsync()
{
await Task.Delay(1000);
Console.WriteLine("Hello");
}

asyncは「このメソッドの中でawaitを使えるようにする」ための修飾子です。asyncを付けただけで自動的に別スレッドで実行されるわけではありません。

9-2. awaitすると処理は遅くなる?

awaitを使うと、非同期処理の完了を待つため、処理の順序としてはそこで待機します。しかし、スレッドをブロックしないため、アプリケーション全体の効率や応答性は改善されることが多いです。

たとえば、HTTP通信を同期的に待つと、その間スレッドが何もできません。

C#
string result = httpClient.GetStringAsync(url).Result;

非同期的に待てば、待機中にスレッドを解放できます。

C#
string result = await httpClient.GetStringAsync(url);

単体の処理時間が劇的に短くなるわけではありません。むしろ、非同期処理にはわずかなオーバーヘッドがあります。しかし、I/O待ちが多い処理では、スレッドを有効活用できるため、結果として応答性やスループットが向上します。

9-3. TaskとThreadは何が違う?

Threadは、実際にコードを実行するスレッドそのものを表します。

C#
Thread thread = new Thread(() =>
{
Console.WriteLine("別スレッドで実行");
});
thread.Start();

一方、Taskは、非同期処理や並行処理を表す抽象的な単位です。

C#
Task task = Task.Run(() =>
{
Console.WriteLine("タスクで実行");
});

Taskは、必ずしも専用のスレッドを意味するわけではありません。I/Oバウンドの非同期処理では、待機中にスレッドを占有しないこともあります。

簡単に言うと、Threadは実行の低レベルな単位、Taskは非同期処理を扱うための高レベルな仕組みです。C#で非同期処理を書く場合、通常はThreadを直接扱うよりも、Taskasync/awaitを使う方が一般的です。

9-4. 非同期処理はどんな場面で使うべき?

非同期処理は、待ち時間が発生する処理で使うのが基本です。

代表的な場面は、HTTP通信、データベースアクセス、ファイル読み書き、外部API呼び出し、クラウドストレージアクセスなどです。

C#
string json = await httpClient.GetStringAsync(url);
C#
string text = await File.ReadAllTextAsync(path);

一方、単純な計算や短時間で終わる処理を無理に非同期化する必要はありません。

C#
int total = a + b;

このような処理にasyncを付けても意味はほとんどありません。

判断基準は、「待ち時間があるか」「スレッドをブロックすると困るか」「非同期APIが用意されているか」です。

9-5. 初心者がまず覚えるべき書き方は?

初心者がまず覚えるべき基本形は、次の3つです。

戻り値がない非同期メソッドです。

C#
public async Task DoSomethingAsync()
{
await Task.Delay(1000);
}

戻り値がある非同期メソッドです。

C#
public async Task<string> GetMessageAsync()
{
await Task.Delay(1000);
return "Hello";
}

呼び出し側の書き方です。

C#
string message = await GetMessageAsync();
Console.WriteLine(message);

そして、次のルールを意識しましょう。

非同期メソッドは基本的にTaskまたはTask<T>を返します。awaitを付け忘れないようにします。.Result.Wait()はなるべく使いません。async voidはイベントハンドラー以外では避けます。非同期メソッド名にはAsyncを付けます。

この基本を押さえれば、C# async/awaitの多くのコードを読み書きできるようになります。

まとめ

C#のasync/awaitは、非同期処理をわかりやすく、安全に書くための重要な構文です。

asyncを付けたメソッドではawaitを使えるようになり、awaitTaskTask<T>の完了を非同期的に待ちます。これにより、HTTP通信、ファイルI/O、データベースアクセスなど、待ち時間のある処理でスレッドをブロックせずに実行できます。

ただし、async/awaitは「自動的に別スレッドで実行する仕組み」ではありません。I/Oバウンド処理では非同期APIをそのままawaitし、CPUバウンド処理では必要に応じてTask.Runを使う、という使い分けが大切です。

また、実務では次のポイントを意識しましょう。

async voidは原則使わず、通常はTaskを返します。非同期メソッド名にはAsyncを付けます。Task.ResultWait()で同期的に待たず、awaitでつなげます。例外はtry-catchで処理し、キャンセルが必要な処理にはCancellationTokenを使います。複数の非同期処理を扱う場合は、Task.WhenAllTask.WhenAnyを活用します。

C# async/awaitは、最初は少し難しく感じるかもしれません。しかし、基本構文とTaskの考え方を理解すれば、読みやすく効率的な非同期コードを書けるようになります。まずは、ファイル読み込みやHTTP通信などの身近な処理からasync/awaitを使ってみると、非同期処理の流れを自然に理解できるようになります。