C#のawaitとは?asyncとの違い・使い方・戻り値・例外処理まで初心者向けに解説

はじめに

C#で非同期処理を学び始めると、ほぼ必ず登場するのがawaitです。HTTP通信、ファイル読み書き、データベースアクセス、UIアプリの画面更新など、時間のかかる処理を扱う場面ではawaitがよく使われます。

しかし初心者のうちは、「awaitは何を待っているのか」「asyncとの違いは何か」「戻り値はどう受け取るのか」「例外はどこで発生するのか」がわかりにくいかもしれません。

この記事では、C#のawaitについて、基本的な意味からasyncとの違い、戻り値、実行順序、例外処理、よくあるエラーまで、初心者向けに順番に解説します。

1. C#のawaitとは?初心者向けに基本をわかりやすく解説

1-1. awaitは非同期処理の完了を待つためのキーワード

C#のawaitは、非同期処理の完了を待つためのキーワードです。

たとえば、Web APIからデータを取得する処理や、ファイルを読み込む処理は、完了までに時間がかかることがあります。そのような処理を通常の同期処理として実行すると、処理が終わるまで次の処理に進めません。

awaitを使うと、非同期処理が完了するまで待ち、完了後にその結果を受け取って次の処理へ進めます。

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

この例では、GetDataAsync()の処理が完了するまで待ち、完了後に戻り値をresultへ代入しています。

1-2. awaitを使うと処理を止めずに結果を待てる

awaitは「待つ」という意味を持ちますが、単純にプログラム全体を止めるわけではありません。

重要なのは、awaitは現在の非同期メソッドの続きの処理を一時的に中断し、待っている間もスレッドを無駄に占有しにくいという点です。

たとえばUIアプリでボタンを押してデータ取得を行う場合、同期的に待ってしまうと画面が固まることがあります。一方、awaitを使えば、通信中も画面の応答性を保ちやすくなります。

C#
private async void Button_Click(object sender, EventArgs e)
{
string data = await DownloadTextAsync();
label.Text = data;
}

このように、時間のかかる処理をawaitすることで、アプリ全体を固めずに処理結果を待てます。

1-3. awaitが必要になる代表的な場面

awaitは、主にI/O処理と呼ばれる時間のかかる処理でよく使われます。

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

C#
// HTTP通信
string html = await httpClient.GetStringAsync("https://example.com");

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

// データベース処理
var user = await dbContext.Users.FindAsync(id);

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

HTTP通信、ファイルアクセス、データベースアクセスなどは、CPUで計算するというより、外部の応答や入出力の完了を待つ処理です。このような処理ではawaitを使うメリットが大きくなります。

1-4. awaitを使わない場合に起こる問題

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

C#
Task<string> task = GetDataAsync();
Console.WriteLine("処理完了");

このコードでは、GetDataAsync()の結果を待っていません。そのため、実際にはデータ取得が終わっていないのに「処理完了」と表示される可能性があります。

また、awaitを書かずに非同期処理を放置すると、例外を適切に捕捉できない、処理順序が意図と違う、警告が出るといった問題が起こります。

非同期メソッドの結果が必要な場合は、基本的にawaitを付けて待つと考えるとわかりやすいです。

2. awaitとasyncの違い

2-1. asyncは非同期メソッドであることを示すキーワード

asyncは、そのメソッド内で非同期処理を扱うことを示すキーワードです。

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

この例では、DoWorkAsyncメソッドにasyncが付いています。これにより、メソッド内でawaitを使えるようになります。

ただし、asyncを付けただけで自動的に処理が別スレッドで実行されるわけではありません。asyncは、主にawaitを使えるようにし、非同期処理を自然な書き方で扱うためのキーワードです。

2-2. awaitは非同期処理の結果を待つキーワード

awaitは、TaskTask<T>などの非同期処理が完了するのを待つキーワードです。

C#
int count = await GetCountAsync();

このコードでは、GetCountAsync()が完了するまで待ち、完了後に結果をcountへ代入します。

つまり、asyncはメソッド側に付けるキーワード、awaitは非同期処理を呼び出す行で使うキーワードです。

2-3. asyncとawaitは基本的にセットで使う

C#では、awaitを使うメソッドには基本的にasyncを付けます。

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

asyncがあるからメソッド内でawaitを使えます。そしてawaitがあるから、非同期処理の完了を自然に待てます。

初心者のうちは、「awaitを使うメソッドにはasyncを付ける」と覚えておくとよいでしょう。

2-4. asyncだけ・awaitだけではどうなるのか

asyncだけを付けて、メソッド内でawaitを使わないことも文法上は可能です。

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

ただし、このようなコードでは「asyncメソッドにawait演算子がありません」という警告が出ます。非同期処理をしていないのにasyncを付けているためです。

一方、asyncが付いていないメソッド内でawaitを使うとエラーになります。

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

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

2-5. async/awaitとTaskの関係

C#の非同期処理では、TaskTask<T>がよく使われます。

Taskは「結果を返さない非同期処理」を表し、Task<T>は「結果を返す非同期処理」を表します。

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

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

awaitは、このTaskTask<T>の完了を待ちます。

Taskawaitした場合は戻り値がありません。Task<T>awaitした場合は、T型の値を受け取れます。

3. C#でawaitを使う基本構文

3-1. awaitの基本的な書き方

awaitの基本構文は次のとおりです。

C#
await 非同期メソッド();

戻り値がある場合は、次のように変数へ代入できます。

C#
 変数名 = await 非同期メソッド();

たとえば、文字列を返す非同期メソッドなら次のようになります。

C#
string message = await GetMessageAsync();

awaitを付けることで、GetMessageAsync()の結果を直接stringとして扱えます。

3-2. Taskを返すメソッドをawaitする例

戻り値が不要な非同期処理では、Taskを返すメソッドを作ります。

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

public async Task ExecuteAsync()
{
await SaveAsync();
Console.WriteLine("次の処理へ進みます");
}

SaveAsync()Taskを返します。await SaveAsync()と書くことで、保存処理が終わってから次の行へ進みます。

3-3. Task<T>を返すメソッドをawaitする例

戻り値がある非同期処理では、Task<T>を使います。

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

public async Task ShowScoreAsync()
{
int score = await GetScoreAsync();
Console.WriteLine($"スコアは{score}です");
}

GetScoreAsync()の戻り値はTask<int>ですが、awaitすると中身のintを受け取れます。

3-4. awaitを使えるメソッドの条件

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

戻り値の型は、主に次のいずれかです。

C#
async Task
async Task<T>
async void

ただし、async voidはイベントハンドラー以外ではできるだけ避けるべきです。例外処理や呼び出し側での待機が難しくなるためです。

通常は、戻り値が不要ならTask、戻り値が必要ならTask<T>を使います。

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

C#では、Mainメソッドをasync Taskにすることで、コンソールアプリのエントリーポイントでもawaitを使えます。

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

class Program
{
static async Task Main()
{
await Task.Delay(1000);
Console.WriteLine("Hello async Main");
}
}

古い書き方ではMainで直接awaitできない場合もありましたが、現在のC#ではasync Task Mainがよく使われます。

4. awaitの戻り値を理解する

4-1. Taskをawaitした場合の戻り値

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

C#
public async Task SendAsync()
{
await Task.Delay(1000);
Console.WriteLine("送信完了");
}

このメソッドをawaitしても、変数に代入する値はありません。

C#
await SendAsync();

「処理が完了するまで待つ」ことが目的であり、結果の値は受け取りません。

4-2. Task<T>をawaitした場合の戻り値

Task<T>awaitすると、T型の値を受け取れます。

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

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

C#
string name = await GetNameAsync();
Console.WriteLine(name);

GetNameAsync()自体の戻り値はTask<string>ですが、awaitするとstringとして扱えます。

4-3. void・Task・Task<T>の違い

C#のメソッドの戻り値として、voidTaskTask<T>は意味が異なります。

C#
void DoWork()
{
// 同期処理
}

async Task DoWorkAsync()
{
// 戻り値なしの非同期処理
await Task.Delay(1000);
}

async Task<int> GetValueAsync()
{
// 戻り値ありの非同期処理
await Task.Delay(1000);
return 123;
}

voidは通常の同期メソッドで使われます。Taskは非同期で完了を待てる処理、Task<T>は非同期で結果も受け取れる処理です。

非同期メソッドでは、基本的にvoidではなくTaskまたはTask<T>を使いましょう。

4-4. 戻り値を変数に代入して使う方法

awaitした結果は、通常の戻り値と同じように変数へ代入できます。

C#
public async Task<int> AddAsync(int a, int b)
{
await Task.Delay(500);
return a + b;
}

public async Task ExecuteAsync()
{
int result = await AddAsync(3, 5);
Console.WriteLine(result);
}

この例では、AddAsync(3, 5)の結果である8resultに入ります。

awaitを使うことで、非同期処理であっても同期処理に近い読みやすいコードを書けます。

4-5. 複数のawait結果を扱う方法

複数の非同期処理の結果を扱う場合、順番にawaitする方法があります。

C#
int a = await GetValueAAsync();
int b = await GetValueBAsync();

Console.WriteLine(a + b);

この場合、GetValueAAsync()が終わってからGetValueBAsync()を開始します。

一方、互いに依存しない処理なら、先にタスクを開始してからまとめて待つこともできます。

C#
Task<int> taskA = GetValueAAsync();
Task<int> taskB = GetValueBAsync();

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

Console.WriteLine(results[0] + results[1]);

このようにすると、複数の処理を同時に進められるため、全体の待ち時間を短くできる場合があります。

5. awaitの実行順序と処理の流れ

5-1. awaitを書くとその行で処理はどう動くのか

awaitを書くと、その非同期処理が完了するまで、現在のメソッドの続きが一時停止します。

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

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

C#
開始
終了

await Task.Delay(1000)の行で約1秒待ち、その後に「終了」が表示されます。

ただし、この待機中にプログラム全体が完全に止まるわけではありません。非同期処理として待機し、完了後に続きの処理が再開されます。

5-2. await中もアプリ全体は止まらない理由

awaitは、待機中にスレッドをブロックしないように設計されています。

たとえば、UIアプリでawaitを使うと、通信やファイル読み込みの完了を待っている間も、画面の描画やクリック操作を処理できる場合があります。

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

await Task.Delay(3000);

button.Enabled = true;
}

このコードでは、3秒待っている間もUIスレッドを完全にブロックしにくいため、アプリが固まりにくくなります。

5-3. 同期処理と非同期処理の違い

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

C#
Thread.Sleep(3000);
Console.WriteLine("完了");

Thread.Sleepはスレッドを停止させます。その間、そのスレッドは他の処理に使えません。

一方、非同期処理では次のように書きます。

C#
await Task.Delay(3000);
Console.WriteLine("完了");

Task.Delayawaitすると、待っている間もスレッドを占有しにくくなります。特にサーバーアプリやUIアプリでは、この違いが重要です。

5-4. awaitを連続で書いた場合の実行順序

awaitを連続で書くと、基本的には上から順番に実行されます。

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

このコードでは、Step1Async()が完了してからStep2Async()、その後にStep3Async()が実行されます。

処理に依存関係がある場合は、この書き方が自然です。

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

この例では、注文情報を取得するためにユーザーIDが必要なので、順番にawaitする必要があります。

5-5. Task.WhenAllで複数処理を並列に待つ方法

互いに依存しない非同期処理なら、Task.WhenAllを使ってまとめて待つことができます。

C#
Task<string> task1 = GetNameAsync();
Task<int> task2 = GetAgeAsync();

await Task.WhenAll(task1, task2);

string name = await task1;
int age = await task2;

Console.WriteLine($"{name}さんは{age}歳です");

Task.WhenAllを使うと、複数のタスクがすべて完了するまで待てます。

順番にawaitすると合計時間が長くなる処理でも、同時に開始すれば待ち時間を短縮できる場合があります。

6. awaitの具体的な使い方

6-1. HTTP通信でawaitを使う例

HTTP通信では、HttpClientの非同期メソッドをawaitして使うことが多いです。

C#
using System.Net.Http;

public async Task<string> GetHtmlAsync()
{
using HttpClient client = new HttpClient();

string html = await client.GetStringAsync("https://example.com");
return html;
}

GetStringAsyncは、指定したURLから文字列を取得する非同期メソッドです。通信が完了するまでawaitで待ち、取得した文字列を返します。

HTTP通信は外部サーバーの応答を待つため、awaitと相性のよい処理です。

6-2. ファイル読み書きでawaitを使う例

ファイルの読み書きにも非同期メソッドがあります。

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

書き込みの場合は次のように書けます。

C#
public async Task WriteFileAsync()
{
await File.WriteAllTextAsync("sample.txt", "こんにちは");
}

大きなファイルを扱う場合や、アプリの応答性を保ちたい場合は、非同期のファイル操作が役立ちます。

6-3. データベース処理でawaitを使う例

Entity Framework CoreなどのORMでは、非同期のデータベース操作がよく使われます。

C#
public async Task<User?> GetUserAsync(int id)
{
User? user = await dbContext.Users.FindAsync(id);
return user;
}

一覧を取得する場合は、次のような形になります。

C#
public async Task<List<User>> GetUsersAsync()
{
List<User> users = await dbContext.Users.ToListAsync();
return users;
}

データベースアクセスも、結果が返るまで待ち時間が発生しやすい処理です。サーバーアプリでは、awaitを使うことでスレッドを効率的に使いやすくなります。

6-4. UIアプリでawaitを使う例

Windows FormsやWPFなどのUIアプリでは、awaitを使うことで画面のフリーズを防ぎやすくなります。

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

await Task.Delay(2000);

label.Text = "完了しました";
}

イベントハンドラーではasync voidが使われることがあります。通常の非同期メソッドではasync voidを避けるべきですが、ボタンクリックなどのイベントハンドラーでは例外的に使われます。

6-5. Task.Delayでawaitの動きを確認する例

Task.Delayは、指定した時間だけ非同期的に待機するメソッドです。awaitの動きを確認する練習に向いています。

C#
public async Task TestAsync()
{
Console.WriteLine("1秒待ちます");

await Task.Delay(1000);

Console.WriteLine("1秒経過しました");
}

Thread.Sleepとは異なり、Task.Delayはスレッドをブロックしにくい待機処理です。

非同期処理の流れを理解したい場合は、まずTask.Delayを使った小さなサンプルから試すとよいでしょう。

7. awaitの例外処理

7-1. await中に例外が発生した場合の動き

awaitしている非同期処理の中で例外が発生すると、その例外はawaitした行で発生したように扱えます。

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

public async Task ExecuteAsync()
{
await ErrorAsync(); // ここで例外が発生したように見える
}

実際には非同期処理の中で例外が発生していますが、呼び出し側ではawaitの行で例外を捕捉できます。

7-2. try-catchでawaitの例外を処理する方法

await中の例外は、通常の例外と同じようにtry-catchで処理できます。

C#
public async Task ExecuteAsync()
{
try
{
await ErrorAsync();
}
catch (InvalidOperationException ex)
{
Console.WriteLine(ex.Message);
}
}

このように書くことで、非同期処理で発生した例外を安全に処理できます。

HTTP通信やデータベース処理では、通信エラーやタイムアウトなどが発生する可能性があるため、必要に応じてtry-catchを使いましょう。

7-3. Taskの例外とawaitの例外の違い

非同期メソッドで例外が発生すると、その例外はTaskに保持されます。

C#
Task task = ErrorAsync();

この時点では、例外はまだ呼び出し側に直接投げられていない場合があります。

その後、awaitすると例外が再スローされます。

C#
await task;

awaitを使うと、Taskに保持されていた例外を自然な形で処理できます。そのため、非同期処理の例外処理では、基本的にawaittry-catchを組み合わせるのがわかりやすいです。

7-4. 複数Taskをawaitした場合の例外処理

Task.WhenAllで複数のタスクを待つ場合、いずれかのタスクで例外が発生すると、await Task.WhenAll(...)の行で例外が発生します。

C#
public async Task ExecuteAsync()
{
try
{
Task task1 = Task1Async();
Task task2 = Task2Async();

await Task.WhenAll(task1, task2);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}

複数のタスクで例外が発生する可能性がある場合、すべての例外を確認したいこともあります。その場合は、各タスクの状態や例外情報を確認する設計が必要です。

初心者のうちは、まずTask.WhenAll全体をtry-catchで囲む形を覚えておくとよいでしょう。

7-5. 例外を握りつぶさないための注意点

非同期処理では、例外を何もせずに無視してしまうと、問題の原因を見つけにくくなります。

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

C#
try
{
await SaveAsync();
}
catch
{
// 何もしない
}

このように空のcatchを書くと、エラーが起きても気づけません。

最低限、ログを出力するなど、原因を追跡できるようにしましょう。

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

例外を処理できない場合は、無理に握りつぶさず、上位に再スローすることも大切です。

8. awaitでよくあるエラーと対処法

8-1. 「awaitはasyncメソッド内でのみ使用できます」の原因

awaitを使うメソッドにasyncが付いていないと、コンパイルエラーになります。

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

対処法は、メソッドにasyncを付けることです。

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

戻り値がない非同期メソッドならasync Task、戻り値があるならasync Task<T>にします。

8-2. 「Taskをawaitしていない」警告の意味

非同期メソッドを呼び出したのにawaitしないと、警告が出ることがあります。

C#
SaveAsync(); // 警告が出る可能性

これは、非同期処理の完了を待っていないためです。処理結果や例外を見逃す可能性があります。

基本的には次のようにawaitします。

C#
await SaveAsync();

意図的に待たない場合もありますが、その場合は例外処理やライフサイクル管理を慎重に設計する必要があります。

8-3. async voidを使ってはいけない理由

async voidは、呼び出し側が完了を待てません。

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

このようなメソッドは、呼び出し側でawaitできないため、例外処理もしにくくなります。

C#
await SaveAsync(); // voidなのでawaitできない

通常の非同期メソッドでは、次のようにTaskを返しましょう。

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

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

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

8-4. awaitを書き忘れたときの不具合

awaitを書き忘れると、処理の順番が意図と変わることがあります。

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

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

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

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

非同期メソッドを呼んだ後に、その処理の結果や完了が必要な場合は、awaitを書き忘れないようにしましょう。

8-5. デッドロックが起こる原因と回避方法

非同期処理でよくある問題に、デッドロックがあります。特に、Task.ResultWait()で非同期処理を同期的に待つと発生することがあります。

悪い例です。

C#
string result = GetDataAsync().Result;

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

C#
GetDataAsync().Wait();

これらはスレッドをブロックして待つため、環境によっては非同期処理の続きが実行できず、デッドロックになることがあります。

基本的な回避方法は、最初から最後までawaitでつなぐことです。

C#
string result = await GetDataAsync();

非同期処理を無理に同期処理へ変換しないことが重要です。

9. awaitを使うときの注意点

9-1. 不要なawaitを増やしすぎない

awaitは便利ですが、何でも付ければよいわけではありません。

たとえば、単に別の非同期メソッドの結果をそのまま返すだけなら、awaitを省略できる場合があります。

C#
public Task<string> GetMessageAsync()
{
return LoadMessageAsync();
}

ただし、例外処理をしたい場合や、usingtry-finally、後続処理がある場合はawaitが必要になることがあります。

読みやすさと必要性のバランスを考えて使いましょう。

9-2. ConfigureAwaitの基本的な考え方

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

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

UIアプリでは、await後にUIを更新するため、元のUIスレッドへ戻る必要があります。そのため、むやみにConfigureAwait(false)を使うと問題になる場合があります。

一方、ライブラリコードなど、特定のコンテキストへ戻る必要がない処理では使われることがあります。

初心者のうちは、まず通常のawaitを理解し、必要になった段階でConfigureAwaitを学ぶとよいでしょう。

9-3. CPU負荷の高い処理とawaitの関係

awaitは、主にI/O待ちの非同期処理に向いています。

一方、CPUを長時間使う重い計算処理は、awaitを付けるだけでは軽くなりません。

C#
public int HeavyCalculation()
{
// CPUを使う重い処理
return 123;
}

このような処理をUIスレッドで実行すると、画面が固まる可能性があります。必要に応じてTask.Runを使い、バックグラウンドで実行することがあります。

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

ただし、サーバーアプリで安易にTask.Runを多用すると、かえって効率が悪くなる場合もあります。

9-4. 非同期処理は必ず速くなるわけではない

asyncawaitを使うと、必ず処理時間が短くなるわけではありません。

非同期処理の主なメリットは、待ち時間中にスレッドを有効活用できることです。

たとえば、HTTP通信を待つ間にスレッドを解放できれば、サーバーでは他のリクエストを処理しやすくなります。UIアプリでは画面の応答性を保ちやすくなります。

しかし、処理そのものの時間が劇的に短くなるとは限りません。非同期処理は「速くするため」だけでなく、「待ち時間を効率よく扱うため」の仕組みだと考えると理解しやすいです。

9-5. 可読性を意識したasync/awaitの書き方

async/awaitを使うときは、読みやすさも大切です。

良い例です。

C#
public async Task<UserProfile> GetUserProfileAsync(int userId)
{
User user = await GetUserAsync(userId);
List<Order> orders = await GetOrdersAsync(userId);

return new UserProfile(user, orders);
}

処理の流れが上から下へ自然に読めます。

一方、タスクを無計画に作ったり、ResultWait()を混ぜたりすると、コードが読みにくくなります。

非同期処理では、メソッド名にAsyncを付ける、戻り値はTaskまたはTask<T>にする、必要なところでawaitする、という基本を守ると可読性が高くなります。

10. awaitと関連キーワードの違い

10-1. awaitとTask.Resultの違い

awaitは非同期処理の完了を非同期的に待ちます。

C#
string result = await GetDataAsync();

一方、Task.Resultは結果が出るまで現在のスレッドをブロックします。

C#
string result = GetDataAsync().Result;

Task.Resultは簡単に見えますが、デッドロックやパフォーマンス低下の原因になることがあります。

非同期メソッドの結果を取得する場合は、基本的にawaitを使いましょう。

10-2. awaitとWaitメソッドの違い

Wait()も、Taskの完了を待つためのメソッドです。

C#
GetDataAsync().Wait();

しかし、Wait()は同期的に待つため、スレッドをブロックします。

awaitは非同期的に待てるため、UIアプリやサーバーアプリではawaitの方が適しています。

C#
await GetDataAsync();

非同期処理の中でWait()を使うのは避け、awaitで統一するのが基本です。

10-3. awaitとTask.Runの違い

awaitは非同期処理の完了を待つキーワードです。

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

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

このコードでは、Task.Runで重い計算処理を別スレッドに渡し、その完了をawaitで待っています。

つまり、Task.Runは処理を開始する側、awaitは完了を待つ側と考えるとわかりやすいです。

10-4. awaitとThread.Sleepの違い

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

C#
Thread.Sleep(1000);

一方、Task.Delayawaitすると、非同期的に待機できます。

C#
await Task.Delay(1000);

待っている間にスレッドを無駄に占有しにくい点が違います。

非同期メソッドの中で時間待ちをしたい場合は、基本的にThread.Sleepではなくawait Task.Delayを使いましょう。

10-5. awaitとyieldの違い

awaityieldはどちらも処理を一時的に中断するように見えますが、目的が異なります。

awaitは非同期処理の完了を待つために使います。

C#
string data = await GetDataAsync();

一方、yield returnは、値を順番に返すイテレーターを作るために使います。

C#
public IEnumerable<int> GetNumbers()
{
yield return 1;
yield return 2;
yield return 3;
}

また、非同期ストリームではawait foreachIAsyncEnumerable<T>と組み合わせて使うこともありますが、初心者のうちはまずawaityieldは別の目的のキーワードだと理解しておけば十分です。

11. awaitを理解するためのサンプルコード集

11-1. 最小構成のasync/awaitサンプル

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

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

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

await Task.Delay(1000);

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

このコードでは、「開始」と表示した後、1秒待ってから「終了」と表示します。

await Task.Delay(1000)により、非同期的な待機を行っています。

11-2. 戻り値ありのawaitサンプル

次は、Task<T>の戻り値をawaitで受け取る例です。

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

class Program
{
static async Task Main()
{
int number = await GetNumberAsync();
Console.WriteLine(number);
}

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

GetNumberAsync()Task<int>を返しますが、awaitすることでintとして受け取れます。

11-3. 例外処理を含むawaitサンプル

非同期処理で例外が発生する例です。

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

class Program
{
static async Task Main()
{
try
{
await ThrowErrorAsync();
}
catch (Exception ex)
{
Console.WriteLine($"例外を捕捉しました: {ex.Message}");
}
}

static async Task ThrowErrorAsync()
{
await Task.Delay(1000);
throw new InvalidOperationException("非同期処理でエラーが発生しました");
}
}

await中に発生した例外は、try-catchで捕捉できます。

11-4. 複数処理を同時に待つサンプル

複数の非同期処理を同時に開始し、Task.WhenAllで待つ例です。

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

class Program
{
static async Task Main()
{
Task<string> task1 = GetNameAsync();
Task<int> task2 = GetAgeAsync();

await Task.WhenAll(task1, task2);

string name = await task1;
int age = await task2;

Console.WriteLine($"{name}さんは{age}歳です");
}

static async Task<string> GetNameAsync()
{
await Task.Delay(1000);
return "佐藤";
}

static async Task<int> GetAgeAsync()
{
await Task.Delay(1000);
return 30;
}
}

このように、互いに依存しない処理は同時に開始すると効率的です。

11-5. awaitを書かない場合との比較サンプル

awaitを書かない場合の例です。

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

class Program
{
static async Task Main()
{
Task task = ShowMessageAsync();

Console.WriteLine("Mainの処理が先に進みます");

await task;
}

static async Task ShowMessageAsync()
{
await Task.Delay(1000);
Console.WriteLine("非同期処理が完了しました");
}
}

ShowMessageAsync()を呼び出しただけでは、完了を待たずに次の行へ進みます。

結果が必要な場合や、処理順序を保証したい場合は、次のようにawaitします。

C#
await ShowMessageAsync();
Console.WriteLine("非同期処理の後に実行されます");

awaitを書くかどうかで、処理の流れが大きく変わることを理解しておきましょう。

12. C#のawaitに関するよくある質問

12-1. awaitは必ずasyncと一緒に使う必要がある?

通常のメソッド内でawaitを使う場合、そのメソッドにはasyncを付ける必要があります。

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

ただし、C#には高度なパターンとしてawait可能な型を作る仕組みもあります。とはいえ初心者のうちは、awaitを使うならasyncを付けると覚えて問題ありません。

12-2. awaitを使うと処理は別スレッドで実行される?

awaitを使ったからといって、必ず別スレッドで実行されるわけではありません。

awaitは、非同期処理の完了を待つための仕組みです。HTTP通信やファイル読み込みのようなI/O処理では、待っている間にスレッドを占有しにくくなります。

別スレッドでCPU処理を実行したい場合は、必要に応じてTask.Runを使います。

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

awaitと別スレッド実行は同じ意味ではない点に注意しましょう。

12-3. awaitを使うとパフォーマンスは上がる?

awaitを使うと、必ず単体の処理速度が上がるわけではありません。

たとえば、1つのHTTP通信に2秒かかる場合、awaitを使っても通信自体が必ず1秒になるわけではありません。

ただし、待ち時間中にスレッドをブロックしにくくなるため、UIの応答性が向上したり、サーバーで多くのリクエストを処理しやすくなったりします。

また、複数の独立した処理をTask.WhenAllで同時に待てば、全体の待ち時間を短縮できる場合があります。

12-4. awaitできる型には何がある?

代表的には、TaskTask<T>awaitできます。

C#
await Task.Delay(1000);

int value = await GetValueAsync();

また、ValueTaskValueTask<T>などもawaitできます。

C#
ValueTask<int> task = GetValueTaskAsync();
int result = await task;

初心者のうちは、まずTaskTask<T>を理解することが大切です。ほとんどの基本的な非同期処理は、この2つで説明できます。

12-5. 初心者はまず何を覚えればよい?

初心者がC#のawaitを学ぶときは、まず次のポイントを押さえるとよいです。

awaitは非同期処理の完了を待つキーワードです。

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

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

Task<T>awaitすると、T型の値を受け取れます。

Task.ResultWait()で無理に同期的に待つのではなく、基本的にawaitを使います。

この基本を理解できれば、HTTP通信、ファイル操作、データベース処理など、多くの非同期処理を読み書きできるようになります。

まとめ

C#のawaitは、非同期処理の完了を待つための重要なキーワードです。

asyncは非同期メソッドであることを示し、awaitTaskTask<T>の完了を待ちます。Taskawaitした場合は戻り値がなく、Task<T>awaitした場合はT型の値を受け取れます。

awaitを使うことで、HTTP通信、ファイル読み書き、データベース処理、UIアプリの待機処理などを、読みやすく安全に書けます。また、try-catchを使えば、非同期処理中の例外も通常の例外と同じように扱えます。

一方で、async voidの多用、awaitの書き忘れ、Task.ResultWait()による同期的な待機には注意が必要です。これらは例外処理の難しさやデッドロックの原因になることがあります。

初心者はまず、async Taskasync Task<T>awaitTask.WhenAlltry-catchの基本を押さえるとよいでしょう。awaitを正しく理解すれば、C#の非同期処理をより自然で読みやすく書けるようになります。