C#のawaitで処理を待つ方法|Task・Delay・WhenAllの使い方と注意点

はじめに

C#で非同期処理を書くときによく使うのがawaitです。awaitは、時間のかかる処理が終わるまで「待つ」ためのキーワードですが、単に処理を止めるだけの仕組みではありません。

たとえば、Web APIからデータを取得する、ファイルを読み込む、データベースへアクセスする、一定時間だけ待機する、といった処理では、完了までに時間がかかることがあります。このような場面でawaitを使うと、処理の完了を待ちながら、スレッドを無駄に占有しない非同期処理を書けます。

この記事では、C#のawaitで処理を待つ基本から、TaskTask<T>Task.Delayの使い方、そしてThread.Sleepとの違いや注意点まで、初心者にもわかりやすく解説します。

1. C#のawaitで処理を待つとは?

1-1. awaitの基本的な役割

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

たとえば、次のように書きます。

C#
await SomeAsyncMethod();

このコードは、SomeAsyncMethodという非同期メソッドの処理が完了するまで待ちます。

ただし、ここで重要なのは、awaitは単純にプログラム全体を停止させるわけではないという点です。awaitは、非同期処理が終わるまで現在のメソッドの続きの処理を一時的に中断し、完了後に再開します。

たとえば、次のようなイメージです。

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

await Task.Delay(1000);

Console.WriteLine("終了");

この場合、Task.Delay(1000)によって約1秒待ったあと、Console.WriteLine("終了")が実行されます。

つまり、awaitは「この処理が終わるまで次へ進まない」という意味で使えます。

1-2. asyncメソッドとawaitの関係

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

C#
public async Task SampleAsync()
{
await Task.Delay(1000);
Console.WriteLine("1秒待ちました");
}

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

awaitだけを単独で使うことはできません。通常は、次のようにasyncawaitをセットで使います。

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

また、asyncメソッドの戻り値には、主に次のような型を使います。

C#
Task
Task<T>
ValueTask
ValueTask<T>

戻り値がない非同期メソッドではTask、結果を返す非同期メソッドではTask<T>を使うのが一般的です。

1-3. 「処理を止める」と「非同期で待つ」の違い

C#で「待つ」と聞くと、Thread.Sleepを思い浮かべる人もいるかもしれません。

C#
Thread.Sleep(1000);

これは、現在のスレッドを1秒間停止させる処理です。

一方、await Task.Delay(1000)は、非同期的に1秒待つ処理です。

C#
await Task.Delay(1000);

この2つは似ているように見えますが、動きは大きく異なります。

Thread.Sleepは、実行中のスレッドを完全にブロックします。その間、そのスレッドは他の処理に使えません。GUIアプリで使うと画面が固まる原因になり、Webアプリで多用するとサーバーの処理効率が悪くなります。

一方、await Task.Delayはスレッドをブロックしません。待機中はスレッドを解放できるため、他の処理を実行できます。

つまり、C#のawaitで待つ場合は、「スレッドを止める」のではなく、「非同期処理の完了までメソッドの続きを待機する」と考えると理解しやすくなります。

1-4. awaitが使える戻り値の種類

awaitは、主にTaskTask<T>に対して使います。

C#
await Task.Delay(1000);

これはTaskを待っています。

戻り値がある場合は、Task<T>awaitします。

C#
int result = await GetNumberAsync();

この場合、GetNumberAsyncTask<int>を返すメソッドです。

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

awaitすると、Task<int>の中に入っているint型の値を取り出せます。

そのため、次のように書けます。

C#
Task<int> task = GetNumberAsync();
int result = await task;

awaitできる代表的な型は次のとおりです。

C#
Task
Task<T>
ValueTask
ValueTask<T>

初心者のうちは、まずTaskTask<T>をしっかり理解すれば十分です。

2. awaitでTaskの完了を待つ基本的な書き方

2-1. Taskをawaitして結果を待つ方法

戻り値がない非同期処理を待つ場合は、Taskawaitします。

C#
public async Task RunAsync()
{
await DoSomethingAsync();
Console.WriteLine("処理が完了しました");
}

public async Task DoSomethingAsync()
{
await Task.Delay(1000);
Console.WriteLine("DoSomethingAsyncの処理");
}

この例では、DoSomethingAsyncの完了を待ってから、Console.WriteLine("処理が完了しました")が実行されます。

実行順序は次のようになります。

C#
DoSomethingAsyncの処理
処理が完了しました

awaitを付けることで、非同期処理が終わる前に次の行へ進むことを防げます。

逆に、awaitを付けずに呼び出すと、処理の完了を待たずに次へ進む場合があります。

C#
DoSomethingAsync();
Console.WriteLine("先に実行される可能性があります");

このような書き方は、意図しない動作や警告の原因になります。非同期メソッドの完了を待ちたい場合は、必ずawaitを付けましょう。

2-2. Task<T>の戻り値を受け取る方法

値を返す非同期処理では、Task<T>を使います。

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

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

このメソッドを呼び出して結果を受け取るには、次のようにawaitします。

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

GetMessageAsyncの戻り値はTask<string>ですが、awaitすることで中身のstringを受け取れます。

数値を返す場合も同じです。

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

public async Task RunAsync()
{
int result = await CalculateAsync();
Console.WriteLine(result);
}

Task<T>を使うと、時間のかかる処理の完了を待ちながら、結果を自然な形で受け取れます。

2-3. 複数行の処理を順番に待つ書き方

複数の非同期処理を順番に実行したい場合は、それぞれにawaitを付けます。

C#
public async Task RunAsync()
{
await Step1Async();
await Step2Async();
await Step3Async();

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

この場合、Step1Asyncが終わってからStep2AsyncStep2Asyncが終わってからStep3Asyncが実行されます。

順番に待つ必要がある処理では、この書き方がわかりやすく安全です。

C#
public async Task Step1Async()
{
await Task.Delay(500);
Console.WriteLine("Step1完了");
}

public async Task Step2Async()
{
await Task.Delay(500);
Console.WriteLine("Step2完了");
}

public async Task Step3Async()
{
await Task.Delay(500);
Console.WriteLine("Step3完了");
}

一方で、それぞれの処理が独立していて同時に実行してもよい場合は、Task.WhenAllを使うと効率的です。

C#
public async Task RunAsync()
{
Task task1 = Step1Async();
Task task2 = Step2Async();
Task task3 = Step3Async();

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

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

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

順番に実行する必要があるならawaitを1つずつ書き、同時に実行できるならTask.WhenAllを使う、という使い分けが大切です。

2-4. awaitを使うメソッドの定義例

awaitを使うメソッドは、基本的にasync Taskまたはasync Task<T>として定義します。

戻り値がない場合は、次のように書きます。

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

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

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

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

C#
await SaveAsync();

string data = await LoadAsync();
Console.WriteLine(data);

注意点として、通常のアプリケーションコードではasync voidはなるべく使わないようにします。

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

async voidは呼び出し側で完了を待てず、例外処理もしづらくなります。

イベントハンドラーなど一部の例外を除き、非同期メソッドはasync Taskまたはasync Task<T>で定義するのが基本です。

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

C#では、Mainメソッドでもawaitを使えます。

コンソールアプリで非同期処理を使う場合は、次のようにasync Task Mainと書きます。

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

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

await Task.Delay(1000);

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

このコードでは、プログラム開始後に1秒待ってから「終了」と表示されます。

戻り値を受け取る例は次のとおりです。

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

class Program
{
static async Task Main(string[] args)
{
string message = await GetMessageAsync();
Console.WriteLine(message);
}

static async Task<string> GetMessageAsync()
{
await Task.Delay(1000);
return "非同期処理が完了しました";
}
}

Mainメソッドでawaitを使えるようにしておくと、コンソールアプリでも非同期処理を自然に書けます。

古い書き方では、Mainから非同期メソッドを呼び出して.Wait().Resultで待つ方法もありますが、デッドロックや例外処理の面で注意が必要です。現在のC#では、可能であればasync Task Mainを使うのがわかりやすい書き方です。

3. Task.Delayで一定時間待つ方法

3-1. Thread.SleepではなくTask.Delayを使う理由

C#で一定時間待つ方法には、Thread.SleepTask.Delayがあります。

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

どちらも一見「1秒待つ」処理に見えますが、非同期処理ではTask.Delayを使うのが基本です。

Thread.Sleepは、現在のスレッドを止めます。たとえば、UIスレッドでThread.Sleepを使うと、画面操作ができなくなったり、アプリが固まったように見えたりします。

C#
Thread.Sleep(3000);

この間、スレッドは何もできません。

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

C#
await Task.Delay(3000);

待機中にスレッドをブロックしないため、アプリケーション全体の応答性を保ちやすくなります。

そのため、asyncメソッドの中で一定時間待つ場合は、基本的にThread.Sleepではなくawait Task.Delayを使います。

3-2. Task.Delayの基本構文

Task.Delayの基本構文は次のとおりです。

C#
await Task.Delay(待機時間);

待機時間はミリ秒で指定します。

C#
await Task.Delay(1000);

この場合、1000ミリ秒、つまり約1秒待ちます。

TimeSpanを使って指定することもできます。

C#
await Task.Delay(TimeSpan.FromSeconds(1));

TimeSpanを使うと、秒・分・時間などの単位がわかりやすくなります。

C#
await Task.Delay(TimeSpan.FromMinutes(1));

この場合は、約1分待機します。

数値だけで指定すると単位がわかりにくくなることがあるため、可読性を重視する場合はTimeSpanを使うのもおすすめです。

3-3. 秒数・ミリ秒を指定して待機する例

ミリ秒で待つ場合は、次のように書きます。

C#
await Task.Delay(500);

これは約0.5秒待つ処理です。

1秒待つ場合は次のとおりです。

C#
await Task.Delay(1000);

3秒待つ場合は次のように書けます。

C#
await Task.Delay(3000);

TimeSpanを使う場合は、より意味が明確になります。

C#
await Task.Delay(TimeSpan.FromMilliseconds(500));
await Task.Delay(TimeSpan.FromSeconds(1));
await Task.Delay(TimeSpan.FromSeconds(3));

実際のコード例は次のとおりです。

C#
public async Task WaitSampleAsync()
{
Console.WriteLine("処理開始");

await Task.Delay(TimeSpan.FromSeconds(2));

Console.WriteLine("2秒待ちました");
}

このように、Task.Delayは「一定時間だけ非同期で待つ」処理を書くときに便利です。

ただし、Task.Delayは厳密なタイマー処理ではありません。指定した時間ぴったりに再開されるとは限らず、実行環境やスレッドの状態によって多少ずれることがあります。通常の待機処理では問題ありませんが、高精度な時間制御が必要な場合は別の仕組みを検討しましょう。

3-4. ループ処理の中でawait Task.Delayを使う例

await Task.Delayは、ループ処理の中でもよく使われます。

たとえば、1秒ごとにメッセージを表示する処理は次のように書けます。

C#
public async Task CountAsync()
{
for (int i = 1; i <= 5; i++)
{
Console.WriteLine($"{i}回目の処理");

await Task.Delay(1000);
}

Console.WriteLine("完了");
}

このコードでは、1秒ごとに処理が実行されます。

実行イメージは次のようになります。

C#
1回目の処理
2回目の処理
3回目の処理
4回目の処理
5回目の処理
完了

定期的に状態を確認する処理にも使えます。

C#
public async Task CheckStatusAsync()
{
while (true)
{
Console.WriteLine("状態を確認しています");

await Task.Delay(TimeSpan.FromSeconds(5));
}
}

ただし、無限ループでTask.Delayを使う場合は、必ずキャンセルできる仕組みを用意するのが安全です。キャンセルできない無限ループは、アプリケーション終了時や処理停止時に問題になることがあります。

3-5. キャンセル可能なDelayの書き方

Task.Delayは、CancellationTokenを渡すことでキャンセル可能にできます。

C#
await Task.Delay(1000, cancellationToken);

具体的な例は次のとおりです。

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

public class Sample
{
public async Task RunAsync(CancellationToken cancellationToken)
{
Console.WriteLine("待機開始");

await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken);

Console.WriteLine("待機完了");
}
}

このコードでは、10秒待機する途中でキャンセル要求があると、Task.Delayがキャンセルされます。

キャンセル時にはOperationCanceledExceptionが発生するため、必要に応じてtry-catchで処理します。

C#
public async Task RunAsync(CancellationToken cancellationToken)
{
try
{
Console.WriteLine("待機開始");

await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken);

Console.WriteLine("待機完了");
}
catch (OperationCanceledException)
{
Console.WriteLine("待機がキャンセルされました");
}
}

ループ処理と組み合わせる場合は、次のように書けます。

C#
public async Task WatchAsync(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
Console.WriteLine("監視中...");

await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
}
}

このようにすると、1秒ごとに処理を実行しながら、外部から安全にキャンセルできます。

実際のアプリケーションでは、長時間待つ処理や繰り返し処理にはキャンセル機能を用意しておくと、終了処理やエラー時の制御がしやすくなります。

まとめ

C#で処理を待つ場合、非同期処理ではawaitを使うのが基本です。

awaitは、TaskTask<T>の完了を待ち、処理が終わったあとに次の行を実行します。戻り値がない場合はTask、戻り値がある場合はTask<T>を使い、awaitすることで結果を自然に受け取れます。

一定時間待つ場合は、Thread.Sleepではなくawait Task.Delayを使うのが一般的です。Thread.Sleepはスレッドをブロックしますが、Task.Delayは非同期的に待つため、アプリケーションの応答性を保ちやすくなります。

また、複数の非同期処理を順番に待つ場合はawaitを1つずつ書き、同時に実行してすべての完了を待ちたい場合はTask.WhenAllを使うと効率的です。

C#のawaitは、単に「待つ」ためだけの機能ではなく、非同期処理を読みやすく、安全に書くための重要な仕組みです。TaskTask<T>Task.Delayの基本を押さえておくことで、Webアプリ、デスクトップアプリ、コンソールアプリなど、さまざまな場面で扱いやすい非同期コードを書けるようになります。