C#のasync/awaitとは?非同期処理の基本・使い方・よくある悩みを初心者向けに解説

はじめに

C#でWeb API通信、ファイル読み込み、データベースアクセス、UIアプリのボタンクリック処理などを書くときによく登場するのがasync/awaitです。

async/awaitは、C#で非同期処理を書くための重要な仕組みです。最初は「asyncを付ければ速くなるの?」「awaitを書くと処理は止まるの?」「Taskって何?」と混乱しやすいですが、基本を押さえるとかなり読みやすく安全なコードを書けるようになります。

この記事では、C#のasync/awaitについて、非同期処理の基本から使い方、Taskとの関係、よくあるエラー、初心者がつまずきやすいポイント、実践的なサンプルコードまで順番に解説します。

1. C#のasync/awaitとは?まず押さえたい非同期処理の基本

C#のasync/awaitは、時間のかかる処理を効率よく扱うための構文です。

たとえば、次のような処理は完了までに時間がかかることがあります。

C#
var result = await GetDataAsync();

このコードでは、GetDataAsync()の完了を待っていますが、待っている間にスレッドを無駄に占有し続けないようにできます。

つまり、async/awaitは「時間のかかる処理を待ちつつ、アプリ全体を止めない」ために使われます。

1-1. async/awaitは「非同期処理を同期処理のように書ける」仕組み

従来の非同期処理は、コールバックやイベントを使って書くことが多く、コードが複雑になりやすいという問題がありました。

async/awaitを使うと、非同期処理を次のように上から順に読める形で書けます。

C#
public async Task<string> GetMessageAsync()
{
await Task.Delay(1000);
return "完了しました";
}

このコードは、1秒待ってから文字列を返す非同期メソッドです。

見た目は通常の同期処理に近いですが、内部では非同期的に処理されています。そのため、読みやすさと効率のよさを両立できます。

1-2. 同期処理と非同期処理の違い

同期処理は、1つの処理が終わるまで次の処理に進まない実行方法です。

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

この場合、Thread.Sleep(3000)の間、現在のスレッドは3秒間ブロックされます。

一方、非同期処理では、時間のかかる処理の完了を待っている間に、スレッドを別の処理に使える可能性があります。

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

Task.Delayは待機を表す非同期処理です。awaitしている間、スレッドを占有し続けるわけではありません。

同期処理はシンプルですが、処理が長いとアプリ全体が固まりやすくなります。非同期処理は少し理解が必要ですが、待ち時間の多い処理を効率よく扱えます。

1-3. 非同期処理が必要になる場面:UIフリーズ・API通信・ファイルI/O

非同期処理が特に役立つのは、待ち時間が発生する処理です。

代表的な例は次のとおりです。

C#
// API通信
var response = await httpClient.GetStringAsync(url);

// ファイル読み込み
var text = await File.ReadAllTextAsync("sample.txt");

// 一定時間待機
await Task.Delay(1000);

Windows Forms、WPF、MAUIなどのUIアプリでは、重い処理を同期的に実行すると画面が固まることがあります。ボタンをクリックしたあとにアプリが反応しなくなるのは、UIスレッドが長時間ブロックされているためです。

ASP.NET CoreのようなWebアプリでも、非同期処理は重要です。API通信やデータベースアクセスを非同期化することで、サーバーのスレッドを効率よく使いやすくなります。

1-4. async/awaitで解決できる初心者の悩み

async/awaitを使うと、次のような悩みを解決しやすくなります。

画面が固まる問題を避けられる、API通信中でもアプリの応答性を保てる、ファイル読み込み中にスレッドを無駄に占有しにくい、非同期処理をコールバックより読みやすく書ける、例外処理をtry-catchで自然に扱える、といったメリットがあります。

特に初心者にとって大きいのは、非同期処理を「普通の処理のように上から順に読める」ことです。

2. async/awaitを理解するために必要なTaskの基礎

async/awaitを理解するには、Taskの理解が欠かせません。

C#の非同期メソッドでは、多くの場合、戻り値としてTaskまたはTask<T>を使います。

2-1. Taskとは何か

Taskは、「将来完了する処理」を表す型です。

たとえば、次のメソッドは非同期で処理を行い、完了したことだけを表します。

C#
public async Task SaveAsync()
{
await Task.Delay(1000);
Console.WriteLine("保存しました");
}

このメソッドは値を返しませんが、処理が完了するまで待つことができます。

C#
await SaveAsync();

Taskは「処理中」「完了」「失敗」「キャンセル」などの状態を持ちます。awaitを使うことで、その完了を自然な形で待てます。

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

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

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

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

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

この場合、awaitするとintの値を受け取れます。

C#
int number = await GetNumberAsync();
Console.WriteLine(number);

つまり、同期メソッドでvoidを返すような処理はTaskintstringなどの値を返す処理はTask<int>Task<string>にする、と考えると分かりやすいです。

2-3. void・Task・Task<T>の使い分け

通常の同期メソッドでは、戻り値がない場合にvoidを使います。

C#
public void Print()
{
Console.WriteLine("Hello");
}

しかし、非同期メソッドでは基本的にasync voidではなくasync Taskを使います。

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

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

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

async voidは、主にイベントハンドラーで使います。

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

通常のメソッドでは、async voidは避けるのが基本です。呼び出し元が完了を待てず、例外処理もしにくくなるためです。

2-4. 非同期メソッド名にAsyncを付ける理由

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

C#
GetDataAsync()
SaveAsync()
LoadFileAsync()

Asyncを付けることで、そのメソッドが非同期処理であることが一目で分かります。

たとえば、次の2つのメソッドがある場合、名前だけで使い分けが分かりやすくなります。

C#
public string GetData()
{
return "data";
}

public async Task<string> GetDataAsync()
{
await Task.Delay(1000);
return "data";
}

必須のルールではありませんが、.NETの標準ライブラリでも多く採用されている命名規則です。チーム開発でも読みやすさが上がるため、非同期メソッドにはAsyncを付けることをおすすめします。

3. C# async/awaitの基本的な書き方

ここからは、async/awaitの基本構文を見ていきます。

3-1. asyncキーワードの役割

asyncは、そのメソッドの中でawaitを使えるようにするキーワードです。

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

重要なのは、asyncを付けただけで自動的に別スレッドで実行されるわけではないという点です。

asyncは「このメソッドは非同期処理を含む可能性があり、awaitを使える」という宣言です。実際に非同期的に待つ処理は、awaitする対象によって決まります。

3-2. awaitキーワードの役割

awaitは、TaskTask<T>の完了を待つキーワードです。

C#
await Task.Delay(1000);

awaitを書くと、その非同期処理が完了するまで、メソッドの残りの処理はいったん中断されます。完了後、続きの処理が再開されます。

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

このコードは「開始」と表示し、1秒後に「終了」と表示します。

awaitのポイントは、待っている間にスレッドをブロックし続けないことです。そのため、UIアプリでは画面の応答性を保ちやすくなります。

3-3. asyncメソッドの基本構文

戻り値がない非同期メソッドは、次のように書きます。

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

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

C#
await DoWorkAsync();

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

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

呼び出す側では、awaitした結果を変数に代入できます。

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

3-4. 戻り値がない非同期メソッドの書き方

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

C#
public async Task SaveDataAsync()
{
await Task.Delay(1000);
Console.WriteLine("データを保存しました");
}

このメソッドは値を返しませんが、呼び出し元は完了を待てます。

C#
await SaveDataAsync();
Console.WriteLine("保存後の処理");

awaitを書けば、保存が完了してから次の処理に進みます。

3-5. 戻り値がある非同期メソッドの書き方

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

C#
public async Task<int> CalculateAsync()
{
await Task.Delay(1000);
return 10 + 20;
}

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

C#
int result = await CalculateAsync();
Console.WriteLine(result);

Task<int>を返すメソッドをawaitすると、intの結果が取り出せます。

returnする値はintですが、メソッドの戻り値の型はTask<int>になる点に注意しましょう。

4. async/awaitの使い方をサンプルコードで解説

ここでは、実際によく使う場面をサンプルコードで確認します。

4-1. Task.Delayを使った最小構成のサンプル

まずは最小構成のサンプルです。

C#
using System;
using System.Threading.Tasks;

class Program
{
static async Task Main()
{
Console.WriteLine("開始");

await WaitAsync();

Console.WriteLine("終了");
}

static async Task WaitAsync()
{
await Task.Delay(1000);
Console.WriteLine("1秒待ちました");
}
}

実行結果は次のようになります。

C#
開始
1秒待ちました
終了

Task.Delay(1000)は1秒待機する非同期処理です。Thread.Sleep(1000)と違い、待機中にスレッドをブロックし続けない点が特徴です。

4-2. API通信・HTTPリクエストでの使い方

Web APIを呼び出す場合、HttpClientの非同期メソッドを使います。

C#
using System;
using System.Net.Http;
using System.Threading.Tasks;

class Program
{
static async Task Main()
{
string result = await GetPageAsync("https://example.com");
Console.WriteLine(result);
}

static async Task<string> GetPageAsync(string url)
{
using var client = new HttpClient();
string content = await client.GetStringAsync(url);
return content;
}
}

GetStringAsyncはHTTPリクエストを非同期で実行します。

API通信はネットワークの待ち時間が発生するため、async/awaitと相性がよい処理です。

実際のアプリでは、HttpClientを毎回生成せず、再利用する設計にすることが多いです。ASP.NET CoreではIHttpClientFactoryを使う方法もよく使われます。

4-3. ファイル読み込みでの使い方

ファイル読み込みも非同期で書けます。

C#
using System;
using System.IO;
using System.Threading.Tasks;

class Program
{
static async Task Main()
{
string text = await ReadFileAsync("sample.txt");
Console.WriteLine(text);
}

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

大きなファイルを読み込む場合、同期的に読み込むとアプリの応答性が下がることがあります。

非同期ファイルI/Oを使うことで、ファイルの読み書き中にスレッドを無駄に占有しにくくなります。

4-4. UIアプリで画面を固めない使い方

Windows FormsやWPFなどのUIアプリでは、ボタンクリック時に重い処理を同期的に実行すると画面が固まります。

たとえば、次のように書くとUIスレッドをブロックします。

C#
private void Button_Click(object sender, EventArgs e)
{
Thread.Sleep(3000);
label1.Text = "完了";
}

非同期処理にすると、画面の応答性を保ちやすくなります。

C#
private async void Button_Click(object sender, EventArgs e)
{
label1.Text = "処理中...";

await Task.Delay(3000);

label1.Text = "完了";
}

イベントハンドラーではasync voidを使うことがあります。ただし、通常のメソッドではasync Taskを使うのが基本です。

4-5. 複数の非同期処理を順番に実行する方法

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

C#
await Step1Async();
await Step2Async();
await Step3Async();

この場合、Step1Asyncが完了してからStep2Asyncが実行され、Step2Asyncが完了してからStep3Asyncが実行されます。

依存関係がある処理では、このように逐次実行するのが自然です。

C#
var user = await GetUserAsync();
var orders = await GetOrdersAsync(user.Id);
var report = await CreateReportAsync(user, orders);

前の処理の結果を次の処理で使う場合は、順番にawaitする必要があります。

5. 複数の非同期処理を効率よく実行する方法

非同期処理は、順番に実行するだけでなく、複数の処理を同時に開始して効率よく待つこともできます。

5-1. awaitを連続で書いた場合の動き

次のコードを見てください。

C#
var result1 = await GetData1Async();
var result2 = await GetData2Async();
var result3 = await GetData3Async();

この場合、処理は順番に実行されます。

GetData1Asyncが完了してからGetData2Asyncを開始し、GetData2Asyncが完了してからGetData3Asyncを開始します。

それぞれ1秒かかる場合、合計で約3秒かかります。

処理同士に依存関係がないなら、同時に開始したほうが効率的です。

5-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つの非同期処理を先に開始し、すべて完了するまで待ちます。

サンプル全体は次のようになります。

C#
static async Task Main()
{
Task<string> task1 = GetDataAsync("A");
Task<string> task2 = GetDataAsync("B");
Task<string> task3 = GetDataAsync("C");

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

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

static async Task<string> GetDataAsync(string name)
{
await Task.Delay(1000);
return $"{name}のデータ";
}

この場合、3つの処理はほぼ同時に開始されるため、合計時間を短縮できます。

ただし、Task.WhenAllは必ずしも「複数スレッドでCPU処理を並列実行する」という意味ではありません。I/O待ちの処理を同時に進める、というイメージで理解するとよいです。

5-3. Task.WhenAnyで先に終わった処理を扱う方法

複数の非同期処理のうち、最初に終わったものを扱いたい場合はTask.WhenAnyを使います。

C#
Task<string> task1 = GetDataAsync("A", 3000);
Task<string> task2 = GetDataAsync("B", 1000);
Task<string> task3 = GetDataAsync("C", 2000);

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

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

メソッドは次のように書けます。

C#
static async Task<string> GetDataAsync(string name, int delay)
{
await Task.Delay(delay);
return $"{name}が完了";
}

Task.WhenAnyは、タイムアウト処理や複数候補のうち最初に返ってきた結果を使いたい場合に便利です。

5-4. 逐次実行と並列実行の使い分け

逐次実行が向いているのは、前の処理の結果を次の処理で使う場合です。

C#
var user = await GetUserAsync();
var orders = await GetOrdersAsync(user.Id);

この場合、user.Idが必要なので、GetUserAsyncが終わる前にGetOrdersAsyncを開始できません。

一方、依存関係がない処理は同時に開始できます。

C#
Task<string> userTask = GetUserNameAsync();
Task<string> weatherTask = GetWeatherAsync();
Task<string> newsTask = GetNewsAsync();

await Task.WhenAll(userTask, weatherTask, newsTask);

並列実行は便利ですが、APIやデータベースに大量のリクエストを一気に送ると負荷が高くなることがあります。必要に応じて同時実行数を制限しましょう。

5-5. パフォーマンス改善時の注意点

非同期処理を使えば必ず速くなるわけではありません。

async/awaitは、主に待ち時間のある処理を効率化するための仕組みです。CPUを大量に使う計算処理は、async/awaitだけでは速くなりません。

また、何でもTask.WhenAllで同時実行すればよいわけでもありません。

たとえば、1000件のAPIを一斉に呼び出すと、相手サーバーに負荷をかけたり、自分のアプリのメモリ使用量が増えたりします。

パフォーマンス改善では、次の点を意識しましょう。

非同期化する対象がI/O待ちなのかCPU処理なのかを見極める、同時実行数を増やしすぎない、不要なTask.Runを使わない、ResultWaitでブロックしない、例外やキャンセルも考慮する、といった点が重要です。

6. 初心者がasync/awaitでつまずきやすいポイント

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

6-1. awaitを書き忘れるとどうなるか

非同期メソッドを呼び出すときにawaitを書き忘れると、処理の完了を待たずに次へ進みます。

C#
SaveAsync();
Console.WriteLine("保存完了");

このコードでは、SaveAsync()の完了前に「保存完了」と表示される可能性があります。

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

C#
await SaveAsync();
Console.WriteLine("保存完了");

また、戻り値がTask<T>のメソッドでawaitを書き忘れると、値ではなくTask<T>そのものを扱うことになります。

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

結果のstringがほしい場合は、次のようにします。

C#
string name = await GetNameAsync();

6-2. async voidを避けるべき理由

async voidは、通常の非同期メソッドでは避けるべきです。

C#
public async void SaveAsync()
{
await Task.Delay(1000);
throw new Exception("エラー");
}

async voidにすると、呼び出し元が処理の完了を待てません。また、例外を呼び出し元のtry-catchで扱いにくくなります。

基本はasync Taskを使います。

C#
public async Task SaveAsync()
{
await Task.Delay(1000);
throw new Exception("エラー");
}

呼び出し元では次のように例外を捕捉できます。

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

ただし、UIのイベントハンドラーではasync voidを使うことがあります。

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

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

非同期処理の結果を同期的に待つために、ResultWait()を使いたくなることがあります。

C#
var result = GetDataAsync().Result;

または次のようなコードです。

C#
GetDataAsync().Wait();

これは避けるべきです。

特にUIアプリや古いASP.NET環境では、デッドロックの原因になることがあります。

awaitは処理の続きを元のコンテキストで再開しようとすることがあります。しかし、呼び出し側が.Result.Wait()でスレッドをブロックしていると、続きの処理が実行できず、互いに待ち続ける状態になることがあります。

正しくは、呼び出し側もasyncにしてawaitします。

C#
var result = await GetDataAsync();

非同期処理は途中で同期的に止めず、できるだけ最後までasync/awaitでつなげるのが基本です。

6-4. 例外処理をどこに書けばよいか

async/awaitでも、基本的には通常のtry-catchと同じように例外処理を書けます。

C#
try
{
string data = await GetDataAsync();
Console.WriteLine(data);
}
catch (HttpRequestException ex)
{
Console.WriteLine($"通信エラー: {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"予期しないエラー: {ex.Message}");
}

ポイントは、例外が発生する非同期処理をawaitする場所でtry-catchを書くことです。

awaitしないままにすると、期待した場所で例外を捕捉できないことがあります。

C#
try
{
var task = GetDataAsync();
}
catch
{
// ここでは捕捉できない場合がある
}

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

C#
try
{
var data = await GetDataAsync();
}
catch
{
Console.WriteLine("エラーを捕捉しました");
}

6-5. 非同期処理なのに速くならない原因

async/awaitを使っても処理が速くならないことがあります。

主な原因は、処理がI/O待ちではなくCPU処理だからです。

たとえば、大量の計算処理はasyncにしただけでは速くなりません。

C#
public async Task<int> CalculateAsync()
{
int result = 0;

for (int i = 0; i < 100000000; i++)
{
result += i;
}

return result;
}

このコードはasyncを付けても、内部でawaitしていないため非同期処理としてのメリットがありません。

CPU負荷の高い処理をUIスレッドから逃がしたい場合は、Task.Runを検討します。

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

ただし、サーバーサイドでTask.Runを乱用すると、かえってスレッドプールを圧迫することがあるため注意が必要です。

7. async/awaitのよくあるエラーと対処法

ここでは、async/awaitでよく見るエラーと対処法を紹介します。

7-1. 「awaitはasyncメソッド内でのみ使用できます」の対処法

次のようなコードを書くとエラーになります。

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

awaitを使うメソッドにはasyncを付ける必要があります。

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

また、呼び出し元でawaitしたい場合も、そのメソッドがasyncである必要があります。

C#
static async Task Main()
{
await SampleAsync();
}

C#のコンソールアプリでは、static async Task Main()を使えます。

7-2. 「非同期メソッドにawaitがありません」の対処法

次のようなコードでは、警告が出ることがあります。

C#
public async Task<int> GetNumberAsync()
{
return 10;
}

asyncが付いているのに、メソッド内でawaitを使っていないためです。

本当に非同期処理が不要なら、asyncを外してTask.FromResultを使えます。

C#
public Task<int> GetNumberAsync()
{
return Task.FromResult(10);
}

または、実際に非同期処理が必要ならawaitを使います。

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

asyncは付ければよいというものではありません。メソッド内に実際の非同期処理があるかを確認しましょう。

7-3. 戻り値の型が合わないときの対処法

非同期メソッドでは、戻り値の型に注意が必要です。

次のコードは誤りです。

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

asyncメソッドでintを返したい場合は、戻り値の型をTask<int>にします。

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

戻り値がない場合はTaskです。

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

async voidはイベントハンドラー以外では基本的に避けましょう。

7-4. 例外がcatchできないときの対処法

例外がcatchできない原因として多いのは、awaitしていないことです。

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

このコードでは、ThrowErrorAsync()の完了を待っていないため、期待どおりに例外を捕捉できないことがあります。

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

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

非同期メソッドの例外は、awaitしたタイミングで再スローされます。そのため、awaitを含めてtry-catchで囲むことが重要です。

7-5. UIスレッド関連のエラーと対処法

UIアプリでは、UI部品を更新できるのは基本的にUIスレッドです。

Task.Runの中から直接UIを更新しようとすると、エラーになることがあります。

C#
await Task.Run(() =>
{
label1.Text = "完了"; // UIスレッド以外から更新しているため問題になる
});

正しくは、重い処理だけをTask.Runで実行し、await後にUIを更新します。

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

label1.Text = $"結果: {result}";

await後にUIスレッドへ戻る環境では、このように書けば安全にUIを更新できます。

ただし、ConfigureAwait(false)を使うと、await後に元のUIスレッドへ戻らない場合があります。UI更新が必要な場所では注意しましょう。

8. async/awaitを安全に使うためのベストプラクティス

async/awaitを安全に使うには、いくつかの基本方針を守ることが大切です。

8-1. 基本はasync Taskを使う

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

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

戻り値がある場合はTask<T>です。

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

async voidはイベントハンドラー以外では避けましょう。

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

Taskを返しておけば、呼び出し元が完了を待てますし、例外処理もしやすくなります。

8-2. 非同期処理は最後までasyncでつなげる

非同期メソッドを途中で同期的に待つのは避けましょう。

避けたい例は次のようなコードです。

C#
var result = GetDataAsync().Result;

正しくは、呼び出し元もasyncにしてawaitします。

C#
var result = await GetDataAsync();

非同期処理は、呼び出し元へ向かってasyncが広がっていくことがあります。これを避けようとして.Result.Wait()を使うと、デッドロックやパフォーマンス低下の原因になります。

「asyncは上まで伝播させる」と覚えておくとよいです。

8-3. ブロッキング処理を混ぜない

async/awaitを使っているコードに、ブロッキング処理を混ぜるとメリットが薄れます。

避けたい例です。

C#
await Task.Delay(1000);
Thread.Sleep(1000);

Thread.Sleepは現在のスレッドを止めます。非同期的に待ちたい場合はTask.Delayを使います。

C#
await Task.Delay(1000);

また、ファイルやHTTP通信でも、可能であれば非同期版のメソッドを使います。

C#
var text = await File.ReadAllTextAsync("sample.txt");
var html = await httpClient.GetStringAsync("https://example.com");

同期メソッドと非同期メソッドを混在させると、思ったほどスケーラビリティが上がらないことがあります。

8-4. ConfigureAwait(false)の使いどころ

ConfigureAwait(false)は、await後に元のコンテキストへ戻る必要がない場合に使われることがあります。

C#
public async Task<string> GetDataAsync()
{
using var client = new HttpClient();
string result = await client.GetStringAsync("https://example.com")
.ConfigureAwait(false);
return result;
}

ライブラリコードなど、UIスレッドへ戻る必要がない処理では、ConfigureAwait(false)を使うことで余計なコンテキスト復帰を避けられる場合があります。

一方、UIアプリでawait後に画面を更新する場合は注意が必要です。

C#
await LoadAsync().ConfigureAwait(false);

// UIスレッドに戻っていない可能性がある
label1.Text = "完了";

UI更新が必要なコードでは、むやみにConfigureAwait(false)を使わないほうが安全です。

ASP.NET Coreでは、従来のASP.NETのような同期コンテキストが基本的にないため、アプリケーションコードで必ずConfigureAwait(false)を付けなければならない、というわけではありません。

8-5. キャンセル処理にはCancellationTokenを使う

時間のかかる非同期処理では、キャンセルできるようにしておくと安全です。

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

await Task.Delay(1000, cancellationToken);
Console.WriteLine($"{i + 1}回目の処理");
}
}

呼び出し側では、CancellationTokenSourceを使います。

C#
using var cts = new CancellationTokenSource();

try
{
await DoWorkAsync(cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("キャンセルされました");
}

一定時間で自動キャンセルしたい場合は、次のように書けます。

C#
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));

await DoWorkAsync(cts.Token);

API通信、ファイル処理、長時間のループ処理では、キャンセル対応を検討しましょう。

9. async/awaitとTask.Runの違い

async/awaitTask.Runは混同されやすいですが、役割が異なります。

9-1. Task.Runは何をするメソッドか

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

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

CPUを使う重い処理をUIスレッドで実行すると画面が固まります。そのような場合に、Task.Runで別スレッドに逃がすことがあります。

一方、async/awaitは非同期処理を読みやすく書くための構文です。必ず別スレッドを作るものではありません。

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

I/Oバウンド処理とは、通信、ファイル、データベースなど、外部の応答待ちが主な処理です。

C#
await httpClient.GetStringAsync(url);
await File.ReadAllTextAsync(path);

このような処理は、async/awaitだけで自然に非同期化できます。

CPUバウンド処理とは、計算や画像処理など、CPUを多く使う処理です。

C#
int result = HeavyCalculation();

CPUバウンド処理は、asyncを付けるだけでは軽くなりません。UIアプリで画面を固めたくない場合などは、Task.Runを使うことがあります。

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

9-3. async/awaitだけでよいケース

API通信、データベースアクセス、ファイル読み書きなどは、非同期版のメソッドが用意されていることが多いです。

C#
var response = await httpClient.GetStringAsync(url);
var text = await File.ReadAllTextAsync(path);

このような場合、わざわざTask.Runで包む必要はありません。

避けたい例です。

C#
var text = await Task.Run(() => File.ReadAllText("sample.txt"));

非同期版があるなら、次のように書くほうが自然です。

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

I/O処理では、まず非同期APIが用意されていないかを確認しましょう。

9-4. Task.Runを使うべきケース

Task.Runが向いているのは、CPU負荷の高い処理をUIスレッドから切り離したい場合です。

C#
private async void Button_Click(object sender, EventArgs e)
{
label1.Text = "計算中...";

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

label1.Text = $"結果: {result}";
}

private int HeavyCalculation()
{
int total = 0;

for (int i = 0; i < 100000000; i++)
{
total += i;
}

return total;
}

このようにすると、重い計算中でもUIスレッドをブロックしにくくなります。

ただし、ASP.NET Coreのリクエスト処理内で安易にTask.Runを使うのは避けるべきです。サーバーではスレッドプールを消費し、かえって効率が悪くなることがあります。

9-5. Task.Runの乱用で起きる問題

Task.Runを乱用すると、次のような問題が起きることがあります。

スレッドプールを無駄に消費する、コンテキスト切り替えのコストが増える、コードが複雑になる、I/O処理を非同期化したつもりでも実際はスレッドを占有している、サーバーアプリでスケーラビリティが下がる、といった問題です。

特に次のような書き方は避けましょう。

C#
await Task.Run(async () =>
{
await httpClient.GetStringAsync(url);
});

HTTP通信にはすでに非同期メソッドがあるため、Task.Runで包む必要はありません。

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

C#
await httpClient.GetStringAsync(url);

Task.Runは「非同期処理にする魔法」ではなく、「別スレッドで処理を実行するための手段」と理解しましょう。

10. async/awaitの理解を深める実践例

ここでは、実際の開発で使いやすいパターンを紹介します。

10-1. ボタンクリックで非同期処理を実行する例

UIアプリでボタンをクリックしたときに、非同期処理を実行する例です。

C#
private async void Button_Click(object sender, EventArgs e)
{
button1.Enabled = false;
label1.Text = "読み込み中...";

try
{
string data = await LoadDataAsync();
label1.Text = data;
}
catch (Exception ex)
{
label1.Text = $"エラー: {ex.Message}";
}
finally
{
button1.Enabled = true;
}
}

private async Task<string> LoadDataAsync()
{
await Task.Delay(2000);
return "読み込み完了";
}

処理中はボタンを無効化し、完了後に再度有効化しています。try-catch-finallyを使うことで、エラーが起きてもボタンが無効のままにならないようにしています。

10-2. 複数APIを同時に呼び出す例

複数のAPIを同時に呼び出す場合は、Task.WhenAllが便利です。

C#
public async Task LoadAllAsync()
{
Task<string> userTask = GetApiAsync("https://example.com/users");
Task<string> productTask = GetApiAsync("https://example.com/products");
Task<string> orderTask = GetApiAsync("https://example.com/orders");

string[] results = await Task.WhenAll(userTask, productTask, orderTask);

string users = results[0];
string products = results[1];
string orders = results[2];

Console.WriteLine(users);
Console.WriteLine(products);
Console.WriteLine(orders);
}

private async Task<string> GetApiAsync(string url)
{
using var client = new HttpClient();
return await client.GetStringAsync(url);
}

依存関係のないAPI呼び出しは、順番に待つより同時に開始したほうが効率的です。

ただし、APIのレート制限やサーバー負荷には注意しましょう。

10-3. タイムアウトを設定する例

時間がかかりすぎる処理には、タイムアウトを設定すると安全です。

C#
public async Task<string> GetWithTimeoutAsync(string url)
{
using var client = new HttpClient();
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3));

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

この例では、3秒以内にHTTPリクエストが完了しなければキャンセルされます。

CancellationTokenSourceを使うと、タイムアウトとキャンセルを統一的に扱えます。

10-4. キャンセル可能な処理を書く例

ユーザーがキャンセルボタンを押したときに処理を止めたい場合は、CancellationTokenを使います。

C#
private CancellationTokenSource? _cts;

private async void StartButton_Click(object sender, EventArgs e)
{
_cts = new CancellationTokenSource();

try
{
await LongRunningAsync(_cts.Token);
label1.Text = "完了";
}
catch (OperationCanceledException)
{
label1.Text = "キャンセルされました";
}
finally
{
_cts.Dispose();
_cts = null;
}
}

private void CancelButton_Click(object sender, EventArgs e)
{
_cts?.Cancel();
}

private async Task LongRunningAsync(CancellationToken cancellationToken)
{
for (int i = 1; i <= 10; i++)
{
cancellationToken.ThrowIfCancellationRequested();

await Task.Delay(1000, cancellationToken);
Console.WriteLine($"{i}秒経過");
}
}

ThrowIfCancellationRequestedを使うことで、キャンセル要求があった場合に処理を中断できます。

Task.DelayにもCancellationTokenを渡しているため、待機中でもキャンセルできます。

10-5. 例外処理を含めた実用的なコード例

最後に、API通信、タイムアウト、キャンセル、例外処理を含めた実用的な例です。

C#
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

public class ApiService
{
private readonly HttpClient _httpClient;

public ApiService(HttpClient httpClient)
{
_httpClient = httpClient;
}

public async Task<string> GetDataAsync(
string url,
CancellationToken cancellationToken)
{
try
{
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
cancellationToken,
timeoutCts.Token);

string result = await _httpClient.GetStringAsync(url, linkedCts.Token);

return result;
}
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
{
return "タイムアウトしました";
}
catch (OperationCanceledException)
{
return "キャンセルされました";
}
catch (HttpRequestException ex)
{
return $"通信エラー: {ex.Message}";
}
catch (Exception ex)
{
return $"予期しないエラー: {ex.Message}";
}
}
}

このコードでは、ユーザー操作によるキャンセルと、タイムアウトによるキャンセルを分けて扱っています。

実務では、非同期処理そのものだけでなく、キャンセル、例外、リトライ、ログ出力なども含めて設計することが重要です。

11. C# async/awaitに関するよくある質問

最後に、C# async awaitを学ぶ初心者が疑問に感じやすい点を整理します。

11-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を使えるようにする」ためのキーワード、と考えると分かりやすいです。

11-2. await中はスレッドが止まっているのか

await中にスレッドがずっと止まっているわけではありません。

たとえば、次のコードでは1秒待ちます。

C#
await Task.Delay(1000);

この待機中、現在のスレッドをブロックし続けるわけではありません。非同期処理が完了したあと、メソッドの続きが再開されます。

ただし、CPUを使う同期処理をawaitできるわけではありません。

C#
HeavyCalculation();

このような処理は、実行中にスレッドを使用します。awaitはあくまでTaskなどの非同期処理の完了を待つための仕組みです。

11-3. asyncメソッドは必ずTaskを返すべきか

通常の非同期メソッドでは、基本的にTaskまたはTask<T>を返すべきです。

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

戻り値がある場合はTask<T>です。

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

async voidは、イベントハンドラーなど一部のケースに限定して使います。

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

Taskを返すことで、呼び出し元がawaitでき、例外も扱いやすくなります。

11-4. Consoleアプリでもasync/awaitは使えるか

Consoleアプリでもasync/awaitは使えます。

現在のC#では、Mainメソッドをasync Task Mainとして書けます。

C#
using System;
using System.Threading.Tasks;

class Program
{
static async Task Main()
{
Console.WriteLine("開始");

await Task.Delay(1000);

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

トップレベルステートメントを使う場合は、さらにシンプルに書けます。

C#
Console.WriteLine("開始");

await Task.Delay(1000);

Console.WriteLine("終了");

学習用のサンプルでも、Consoleアプリはasync/awaitの動きを確認しやすい環境です。

11-5. ASP.NET CoreでConfigureAwait(false)は必要か

ASP.NET Coreでは、従来のASP.NETとは異なり、リクエストごとの同期コンテキストに戻る必要が基本的にありません。

そのため、ASP.NET Coreのアプリケーションコードでは、すべてのawaitに必ずConfigureAwait(false)を付ける必要はありません。

C#
public async Task<IActionResult> Index()
{
var data = await _service.GetDataAsync();
return View(data);
}

このように通常のアプリケーションコードでは、シンプルにawaitを書くことが多いです。

一方、汎用ライブラリでは、呼び出し元のコンテキストに依存しないようにConfigureAwait(false)を使うことがあります。

C#
var data = await LoadAsync().ConfigureAwait(false);

初心者のうちは、まずasync/awaitの基本、Taskの使い方、.Result.Wait()を避けることを優先して理解するとよいです。

まとめ

C#のasync/awaitは、非同期処理を読みやすく、安全に書くための重要な仕組みです。

asyncはメソッド内でawaitを使えるようにするキーワードで、awaitTaskTask<T>の完了を待つキーワードです。戻り値がない非同期メソッドではTask、戻り値がある非同期メソッドではTask<T>を使います。

API通信、ファイルI/O、データベースアクセス、UIアプリの処理など、待ち時間が発生する場面ではasync/awaitが役立ちます。一方で、CPU負荷の高い処理はasyncを付けるだけでは速くならず、必要に応じてTask.Runなどを検討します。

初心者が特に注意したいポイントは、awaitを書き忘れないこと、async voidをむやみに使わないこと、.Result.Wait()でブロックしないこと、例外処理はawaitする場所で行うことです。

まずは次の基本形をしっかり覚えましょう。

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

戻り値がある場合は次の形です。

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

C# async awaitの理解は、最初は難しく感じるかもしれません。しかし、Taskasyncawait、例外処理、キャンセル処理を順番に学べば、実務でも使える非同期コードを書けるようになります。