C#のwait完全ガイド|Thread.Sleep・Task.Delay・Waitの違いと正しい使い方

はじめに

C#で「少し待ってから処理したい」「非同期処理の完了を待ちたい」「一定間隔で処理を繰り返したい」と考えたとき、よく検索されるのが「csharp wait」です。

ただし、C#の待機処理には Thread.SleepTask.DelayTask.WaitawaitWaitHandleMonitor.Wait など複数の選択肢があります。名前が似ているため混同されやすいですが、それぞれ役割は大きく異なります。

結論からいうと、現代のC#では、単に一定時間待ちたい場合は await Task.Delay(...) を基本にする のが安全です。Thread.Sleep は現在のスレッドを止める処理、Task.Wait は非同期処理を同期的に待つ処理であり、使い方を誤るとUIフリーズやデッドロック、パフォーマンス低下につながります。

この記事では、C#のwaitに関する代表的な待機処理を、サンプルコードと実務での判断基準を交えて解説します。

1. C#のwaitとは?待機処理で実現できること

C#における「wait」は、特定の構文だけを指す言葉ではありません。英語の「待つ」という意味から、一定時間停止する処理、非同期処理の完了を待つ処理、スレッド間の合図を待つ処理などを広く指して使われます。

C#には wait という予約語はありません。近い名前のものとして、非同期処理で使う awaitTask の完了を同期的に待つ Wait()、スレッドを停止する Thread.Sleep() などがあります。

1-1. C#で「待つ」処理が必要になる代表的な場面

C#で待機処理が必要になる場面は多くあります。

たとえば、次のようなケースです。

場面代表的な待機方法
1秒後に処理を実行したいawait Task.Delay(1000)
コンソールアプリで一時停止したいThread.Sleep(1000) または await Task.Delay(1000)
非同期APIの完了を待ちたいawait
複数の非同期処理をすべて待ちたいawait Task.WhenAll(...)
一定間隔で処理を繰り返したいawait Task.Delay(...)PeriodicTimer
タイムアウトを設定したいTask.WhenAny(...)CancellationTokenSource
スレッド間の合図を待ちたいWaitHandleMonitor.Wait

単に「待つ」といっても、時間を待つのか、処理完了を待つのか、条件成立を待つのかで使うAPIが変わります。

1-2. wait・Sleep・Delay・Waitが混同されやすい理由

Thread.SleepTask.DelayTask.Wait は、いずれも「待つ」処理に見えます。しかし、止めている対象が異なります。

Thread.Sleep は、現在実行中のスレッドを指定時間停止します。Microsoft Learnでも、Thread.Sleep は「指定された時間だけ現在のスレッドを中断する」と説明されています。

一方で、Task.Delay は指定時間後に完了する Task を作るメソッドです。つまり、スレッドそのものを止めるのではなく、「時間が経ったら完了する非同期タスク」を扱います。

Task.Wait は、すでに開始された Task の完了を現在のスレッドで同期的に待つメソッドです。Task.Wait() はタスクが完了するまで待機するため、呼び出し元スレッドをブロックします。

1-3. 同期処理と非同期処理で待機方法が変わる

C#の待機処理を理解するには、「同期処理」と「非同期処理」の違いを押さえる必要があります。

同期処理では、ある処理が終わるまで次の処理に進みません。そのため、Thread.SleepTask.Wait のように呼び出し元スレッドを止める書き方が使われることがあります。

非同期処理では、処理の完了を待っている間もスレッドを占有しない書き方ができます。await は、待機中に現在のスレッドをブロックせず、処理の続きだけを予約して呼び出し元へ制御を戻します。Microsoft Learnでも、await 式は待機対象のタスク実行中に現在のスレッドをブロックしないと説明されています。

1-4. この記事でわかるC#の待機処理の全体像

この記事では、C#のwaitに関する代表的な処理として、次の違いを整理します。

名前主な用途スレッドをブロックするか
Thread.Sleep現在のスレッドを一定時間止めるする
Task.Delay一定時間後に完了するタスクを作るawait すれば基本的にしない
Task.Waitタスク完了を同期的に待つする
await非同期処理の完了を待つ基本的にしない
WaitHandleスレッド間の通知・同期を待つする
Monitor.Waitロックと連動して条件を待つする

2. C#で使う主な待機処理の種類

2-1. Thread.Sleepとは

Thread.Sleep は、現在のスレッドを指定した時間だけ停止するメソッドです。

C#
Thread.Sleep(1000); // 1000ミリ秒 = 1秒待つ

実行中のスレッドそのものを止めるため、待っている間、そのスレッドでは他の処理を実行できません。コンソールアプリや簡単な検証コードでは便利ですが、UIアプリやWebアプリで不用意に使うと問題になりやすいです。

2-2. Task.Delayとは

Task.Delay は、指定した時間が経過したあとに完了する Task を返します。Microsoft Learnでも、Task.Delay は指定時間後に完了するタスクを作成するメソッドと説明されています。

C#
await Task.Delay(1000); // 非同期に1秒待つ

await と組み合わせることで、待機中に呼び出し元スレッドを占有しにくくなります。UIアプリ、ASP.NET Core、非同期APIを扱うコードでは、Thread.Sleep より Task.Delay が適しています。

2-3. Task.Waitとは

Task.Wait は、Task の完了を同期的に待つメソッドです。

C#
Task task = DoSomethingAsync();
task.Wait();

一見便利ですが、呼び出し元のスレッドをブロックします。特にUIスレッドや古いASP.NETの同期コンテキスト上で使うと、デッドロックの原因になることがあります。

2-4. awaitとは

await は、非同期処理の完了を待つためのC#の演算子です。

C#
await DoSomethingAsync();

await は「非同期処理が完了するまで、そのメソッドの続きの実行を一時停止する」ための仕組みです。Task.Wait と違い、通常は現在のスレッドをブロックしません。Microsoft Learnでは、async メソッド内の await は中断点を示し、完了まで先へ進めない間、制御を呼び出し元へ戻すと説明されています。

2-5. WaitHandle・Monitor.Waitなどその他の待機方法

WaitHandle は、OS固有の同期オブジェクトをラップし、共有リソースへの排他的アクセスなどを待つためのクラスです。AutoResetEventManualResetEventMutexSemaphore などが関連します。

Monitor.Wait は、ロックを解放して現在のスレッドを待機させ、再びロックを取得するまでブロックするメソッドです。スレッド間で「条件が満たされるまで待つ」ような低レベルな同期処理に使われます。

ただし、通常のアプリケーション開発では、まず TaskasyncawaitCancellationToken を使う設計を考えるのがおすすめです。

2-6. それぞれの使い分け早見表

やりたいこと推奨される書き方避けたい書き方
非同期で1秒待つawait Task.Delay(1000)Thread.Sleep(1000)
UIを固めずに待つawait Task.Delay(...)UIスレッドで Thread.Sleep
API完了を待つawait ApiCallAsync()ApiCallAsync().Wait()
複数Taskを待つawait Task.WhenAll(tasks)Task.WaitAll(tasks)
コンソール検証で一時停止Thread.Sleep も可本番コードで乱用
タイムアウトを設定するTask.WhenAnyCancellationToken無限の Wait()
スレッド間同期を行うSemaphoreSlimChannelWaitHandleSleep で待ち合わせ

3. Thread.Sleepの使い方と注意点

3-1. Thread.Sleepの基本構文

Thread.Sleep を使うには、System.Threading 名前空間を利用します。

C#
using System;
using System.Threading;

class Program
{
static void Main()
{
Console.WriteLine("開始");
Thread.Sleep(1000);
Console.WriteLine("1秒後に表示");
}
}

ミリ秒で指定するため、1秒待つ場合は 1000、3秒待つ場合は 3000 と書きます。

C#
Thread.Sleep(3000); // 3秒待つ

TimeSpan を使って書くこともできます。

C#
Thread.Sleep(TimeSpan.FromSeconds(3));

3-2. 指定したミリ秒だけ現在のスレッドを停止する仕組み

Thread.Sleep は、実行中の現在のスレッドを一定時間スケジューリング対象から外します。Microsoft Learnでは、指定時間のあいだ、そのスレッドはOSによって実行予定に入れられないと説明されています。

重要なのは、「処理全体をいい感じに待たせる」のではなく、「そのスレッドを止める」という点です。UIスレッドで実行すれば画面操作が止まり、リクエスト処理スレッドで実行すればそのリクエスト処理が止まります。

3-3. Thread.Sleepを使ったサンプルコード

C#
using System;
using System.Threading;

class Program
{
static void Main()
{
for (int i = 1; i <= 5; i++)
{
Console.WriteLine($"{i}回目の処理");
Thread.Sleep(1000);
}

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

このコードでは、1秒ごとにメッセージを表示します。コンソールアプリの簡単な検証であれば問題ありません。

3-4. UIアプリやWebアプリでThread.Sleepを避けるべき理由

WPFやWindows FormsなどのUIアプリでは、画面描画やクリックイベントの処理は主にUIスレッドで行われます。UIスレッド上で Thread.Sleep を実行すると、待機中は画面更新やボタン操作が止まります。

C#
private void Button_Click(object sender, EventArgs e)
{
Thread.Sleep(3000); // UIが3秒固まる
label1.Text = "完了";
}

このような場合は、次のように await Task.Delay を使います。

C#
private async void Button_Click(object sender, EventArgs e)
{
await Task.Delay(3000);
label1.Text = "完了";
}

ASP.NET Coreでも、Thread.Sleep はリクエスト処理中のスレッドを無駄に占有します。高負荷時にスレッドプールを圧迫し、応答性を下げる原因になります。

3-5. Thread.Sleepを使ってよいケース

Thread.Sleep を完全に使ってはいけないわけではありません。次のようなケースでは許容されることがあります。

ケース理由
コンソールアプリの簡易デモUIフリーズの影響が小さい
一時的な検証コード本番品質を求めない
専用バックグラウンドスレッドの制御影響範囲を理解している場合
外部機器制御などで短時間待つ必要がある要件として同期的な待機が必要な場合

ただし、本番コードでは「本当にスレッドを止めたいのか」を必ず確認してください。

3-6. Thread.Sleepのよくある失敗例

よくある失敗は、処理完了を待つために Thread.Sleep を使うことです。

C#
StartBackgroundJob();

Thread.Sleep(5000); // 5秒待てば終わっているはず、という危険な仮定

ReadResult();

このコードは、処理が5秒以内に終わる保証がなければ不安定です。処理が早く終わっても無駄に待ち、遅く終われば失敗します。

正しくは、完了通知や Task を使って待つべきです。

C#
await StartBackgroundJobAsync();
ReadResult();

4. Task.Delayの使い方と正しい待機方法

4-1. Task.Delayの基本構文

Task.DelaySystem.Threading.Tasks 名前空間で使えます。

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

class Program
{
static async Task Main()
{
Console.WriteLine("開始");
await Task.Delay(1000);
Console.WriteLine("1秒後に表示");
}
}

ミリ秒指定のほか、TimeSpan でも指定できます。

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

4-2. await Task.Delayで非同期に待機する仕組み

await Task.Delay(1000) は、1秒間スレッドを占有して止めるのではなく、1秒後に完了するタスクを待ちます。await は待機中に現在のスレッドをブロックせず、メソッドの続きだけを継続処理として登録します。

そのため、UIアプリでは画面が固まりにくく、Webアプリではスレッドを効率よく使いやすくなります。

4-3. Task.Delayを使ったサンプルコード

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

class Program
{
static async Task Main()
{
for (int i = 1; i <= 5; i++)
{
Console.WriteLine($"{i}回目の処理");
await Task.Delay(1000);
}

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

非同期メソッドでは、戻り値を Task または Task<T> にします。

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

4-4. Thread.Sleepとの違い

Thread.SleepTask.Delay の違いは、スレッドを止めるかどうかです。

C#
Thread.Sleep(1000);      // 現在のスレッドを1秒止める
await Task.Delay(1000); // 1秒後に完了するTaskを非同期に待つ

UIアプリ、Webアプリ、非同期処理では、基本的に Task.Delay を使います。Thread.Sleep は同期的な停止が必要なときだけ使います。

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

Task.DelayCancellationToken を受け取るオーバーロードがあります。キャンセル可能なタスクを作れるため、ユーザー操作やタイムアウトに応じて待機を中断できます。

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

static async Task WaitWithCancelAsync(CancellationToken cancellationToken)
{
try
{
await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken);
Console.WriteLine("10秒待機しました");
}
catch (OperationCanceledException)
{
Console.WriteLine("待機がキャンセルされました");
}
}

キャンセルを使うと、アプリ終了時やリクエスト中断時に無駄な処理を残しにくくなります。

4-6. ループ処理・リトライ処理での活用例

一定間隔で処理する場合、Task.Delay はよく使われます。

C#
while (!cancellationToken.IsCancellationRequested)
{
await FetchDataAsync(cancellationToken);
await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);
}

ただし、長期間動く周期処理では PeriodicTimer も選択肢です。PeriodicTimer は非同期にタイマーのtickを待つためのクラスです。

C#
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(5));

while (await timer.WaitForNextTickAsync(cancellationToken))
{
await FetchDataAsync(cancellationToken);
}

5. Task.Waitの使い方と危険なポイント

5-1. Task.Waitの基本構文

Task.Wait は、タスクが完了するまで現在のスレッドを待機させます。

C#
Task task = DoWorkAsync();
task.Wait();

戻り値のある Task<T> では、Result を使って結果を取得できます。

C#
Task<int> task = GetValueAsync();
int value = task.Result;

ただし、Wait()Result は同期的に待つため、非同期コード内では原則として避けるべきです。

5-2. 非同期処理を同期的に待つ仕組み

Task.Wait() は、タスクの完了まで呼び出し元スレッドをブロックします。Microsoft Learnでも、Task.WaitTask の実行完了を待つメソッドと説明されています。

これは、await のように「処理の続きだけを予約して戻る」のではありません。現在のスレッドをその場で止めます。

5-3. Task.Waitを使ったサンプルコード

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

class Program
{
static void Main()
{
Task task = DoWorkAsync();
task.Wait();

Console.WriteLine("完了");
}

static async Task DoWorkAsync()
{
await Task.Delay(1000);
Console.WriteLine("非同期処理完了");
}
}

コンソールアプリの Mainasync にできない古い環境では、このような書き方を見かけることがあります。しかし、現在のC#では async Task Main が使えるため、次のように書くのが自然です。

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

class Program
{
static async Task Main()
{
await DoWorkAsync();
Console.WriteLine("完了");
}

static async Task DoWorkAsync()
{
await Task.Delay(1000);
Console.WriteLine("非同期処理完了");
}
}

5-4. awaitとの違い

Task.Waitawait の違いは、スレッドをブロックするかどうかです。

C#
task.Wait();  // 現在のスレッドをブロックする
await task; // 非同期に完了を待つ

もう一つの違いは例外の扱いです。Task.Wait では、タスク内の例外が AggregateException として扱われることがあります。一方、await では通常、元の例外として扱いやすくなります。

5-5. Task.Waitでデッドロックが起きる理由

UIアプリで次のようなコードを書くと、デッドロックする可能性があります。

C#
private void Button_Click(object sender, EventArgs e)
{
var result = GetDataAsync().Result; // 危険
label1.Text = result;
}

private async Task<string> GetDataAsync()
{
await Task.Delay(1000);
return "完了";
}

UIスレッドは .Result によってブロックされます。一方、await の続きがUIスレッドに戻ろうとすると、UIスレッドはすでにブロックされているため、処理が進まなくなります。

Microsoft Learnの ConfigureAwait の説明でも、継続が元のコンテキストへ戻る挙動は、UIスレッドでのデッドロックにつながる場合があると説明されています。

5-6. Task.Waitを避けてawaitを使うべきケース

次のようなケースでは、Task.Wait ではなく await を使うべきです。

ケース理由
WPF・Windows FormsUIフリーズやデッドロックを避けるため
ASP.NET Coreスレッドブロックによるスケーラビリティ低下を避けるため
ライブラリコード呼び出し元の同期コンテキストを壊さないため
非同期メソッド内async/awaitの流れを維持するため
複数の非同期処理Task.WhenAll のほうが自然なため

5-7. Task.Result・WaitAll・WaitAnyとの違い

Task.Result は、Task<T> の結果を同期的に取得します。未完了なら完了までブロックします。

C#
int value = GetValueAsync().Result;

Task.WaitAll は、複数の Task がすべて完了するまで同期的に待ちます。Microsoft Learnでも、WaitAll は複数の Task オブジェクトすべての完了を待つメソッドと説明されています。

C#
Task.WaitAll(task1, task2, task3);

非同期コードでは、代わりに Task.WhenAll を使います。Task.WhenAll は、すべてのタスクが完了したときに完了するタスクを作成します。

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

Task.WaitAny に相当する非同期の書き方としては、Task.WhenAny が使えます。Task.WhenAny は、渡したタスクのいずれかが完了したときに完了するタスクを作ります。

C#
Task completed = await Task.WhenAny(task1, task2, task3);

6. Thread.Sleep・Task.Delay・Waitの違い

6-1. 処理を止める対象の違い

Thread.Sleep は現在のスレッドを止めます。

Task.Delay は、指定時間後に完了するタスクを作ります。

Task.Wait は、別のタスクの完了を現在のスレッドで待ちます。

つまり、3つはすべて「待つ」ように見えても、待機対象が違います。

6-2. 同期処理・非同期処理の違い

Thread.SleepTask.Wait は同期的な待機です。呼び出した場所で処理が止まります。

await Task.Delay は非同期的な待機です。メソッドの続きは一時停止しますが、スレッドを占有し続けるわけではありません。

6-3. スレッドをブロックするかどうかの違い

方法スレッドブロック
Thread.Sleepする
Task.Delay 単体しない。時間後に完了するTaskを返す
await Task.Delay通常しない
Task.Waitする
Task.Result未完了ならする
await task通常しない

実務では、この「ブロックするかどうか」が非常に重要です。

6-4. パフォーマンスへの影響の違い

Thread.SleepTask.Wait は、待機中もスレッドを占有します。少数なら問題が見えにくいですが、Webアプリや大量の並列処理で多用すると、スレッドプール不足や応答遅延につながります。

await Task.Delay は、待機中にスレッドを解放しやすいため、I/O待ちやタイマー待ちに向いています。

6-5. UIフリーズやデッドロックの起きやすさの違い

UIアプリでは、次の順で危険度が高くなります。

書き方危険度理由
await Task.DelayUIスレッドをブロックしにくい
Thread.SleepUIスレッドを直接止める
Task.Wait / .ResultUIデッドロックの原因になりやすい

UIイベントハンドラでは、基本的に async を付けて await で待つのが安全です。

6-6. 実務での使い分け判断基準

実務では、次の基準で選ぶと判断しやすくなります。

判断ポイント選ぶべき方法
単に時間を待ちたいawait Task.Delay
非同期処理の完了を待ちたいawait
複数処理をすべて待ちたいawait Task.WhenAll
どれか1つ完了するまで待ちたいawait Task.WhenAny
UIを固めたくないawait
Webアプリでスケーラブルにしたいawait
低レベルなスレッド同期をしたいWaitHandleMonitorSemaphoreSlim
簡単な検証で一時停止したいThread.Sleep も可

7. C#のwaitでよくある実装パターン

7-1. 一定時間待ってから処理を実行する

C#
await Task.Delay(TimeSpan.FromSeconds(1));
Console.WriteLine("1秒後に実行");

同期的に止めたいだけなら次の書き方も可能です。

C#
Thread.Sleep(1000);
Console.WriteLine("1秒後に実行");

ただし、実務では await Task.Delay を基本にしましょう。

7-2. ループ内で一定間隔ごとに処理する

C#
while (!cancellationToken.IsCancellationRequested)
{
await DoWorkAsync(cancellationToken);
await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken);
}

処理時間を含めて10秒間隔にしたいのか、処理完了後に10秒待ちたいのかで設計が変わります。

処理完了後に10秒待つ場合は上記で問題ありません。厳密な周期実行が必要な場合は、PeriodicTimer やスケジューラの利用を検討します。

7-3. API呼び出しをリトライする

C#
static async Task<string> GetWithRetryAsync(CancellationToken cancellationToken)
{
const int maxRetries = 3;

for (int attempt = 1; attempt <= maxRetries; attempt++)
{
try
{
return await CallApiAsync(cancellationToken);
}
catch (HttpRequestException) when (attempt < maxRetries)
{
await Task.Delay(TimeSpan.FromSeconds(attempt), cancellationToken);
}
}

throw new InvalidOperationException("API呼び出しに失敗しました");
}

リトライでは、毎回すぐ再実行するのではなく、少し待つことで一時的な障害から回復しやすくなります。実務では指数バックオフやジッターも検討します。

7-4. タイムアウト付きで処理を待つ

C#
static async Task<string> WithTimeoutAsync()
{
Task<string> workTask = LongRunningOperationAsync();
Task timeoutTask = Task.Delay(TimeSpan.FromSeconds(5));

Task completedTask = await Task.WhenAny(workTask, timeoutTask);

if (completedTask == timeoutTask)
{
throw new TimeoutException("処理がタイムアウトしました");
}

return await workTask;
}

Task.WhenAny を使うと、「処理本体」と「タイムアウト用のDelay」のどちらが先に終わったかを判定できます。

7-5. 複数のTaskが完了するまで待つ

C#
Task task1 = DownloadAsync("https://example.com/a");
Task task2 = DownloadAsync("https://example.com/b");
Task task3 = DownloadAsync("https://example.com/c");

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

Console.WriteLine("すべて完了");

同期的な Task.WaitAll より、非同期の Task.WhenAll を使うのが基本です。

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

C#
Task<int> a = GetValueAsync(1);
Task<int> b = GetValueAsync(2);

int[] results = await Task.WhenAll(a, b);

Console.WriteLine(results.Sum());

7-6. 条件を満たすまで待機する

単純なポーリングでは、Task.Delay を組み合わせます。

C#
static async Task WaitUntilReadyAsync(CancellationToken cancellationToken)
{
while (!await IsReadyAsync(cancellationToken))
{
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
}
}

ただし、条件成立を検知できるイベントや通知があるなら、ポーリングよりイベント駆動にするほうが効率的です。

7-7. テストコードで一時的に待機する

テストで次のように書くと、不安定になりやすいです。

C#
await Task.Delay(1000);
Assert.True(service.IsCompleted);

1秒で終わる保証がないため、環境によって失敗します。可能なら、完了を示す Task やイベントを待ちます。

C#
await service.Completion;
Assert.True(service.IsCompleted);

どうしても待つ場合は、タイムアウト付きで条件を待つヘルパーを作ります。

C#
static async Task WaitUntilAsync(Func<bool> condition, TimeSpan timeout)
{
var start = DateTime.UtcNow;

while (!condition())
{
if (DateTime.UtcNow - start > timeout)
{
throw new TimeoutException();
}

await Task.Delay(50);
}
}

8. 非同期処理でwaitを使うときのベストプラクティス

8-1. 基本はawait Task.Delayを使う

非同期処理で一定時間待つなら、まず await Task.Delay を選びます。

C#
await Task.Delay(1000);

Thread.Sleep は、非同期処理の流れを壊し、スレッドを無駄に止めます。Task.Wait は、非同期処理を同期的に待ってしまうため、デッドロックやパフォーマンス低下の原因になります。

8-2. asyncメソッドではTask.Waitを避ける

async メソッド内で次のように書くのは避けます。

C#
public async Task BadAsync()
{
DoWorkAsync().Wait();
}

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

C#
public async Task GoodAsync()
{
await DoWorkAsync();
}

async メソッド内では、非同期処理の流れを最後まで await でつなぐのが原則です。

8-3. CancellationTokenでキャンセル可能にする

長く待つ処理では、キャンセルできるようにします。

C#
public async Task DoWorkAsync(CancellationToken cancellationToken)
{
await Task.Delay(TimeSpan.FromSeconds(30), cancellationToken);
}

CancellationToken は、操作をキャンセルすべきことを通知する仕組みです。

Webリクエスト、バッチ停止、ユーザーによるキャンセル操作などでは、CancellationToken を受け取れる設計にしておくと安全です。

8-4. ConfigureAwaitの使いどころ

ライブラリコードでは、必要に応じて ConfigureAwait(false) を使います。

C#
await Task.Delay(1000).ConfigureAwait(false);

ConfigureAwait(false) は、await 後の継続処理を元の同期コンテキストへ戻そうとしない設定です。Microsoft Learnでは、ConfigureAwait(false) により、元のコンテキストへ戻ることによるパフォーマンスコストやUIスレッドでのデッドロックを避けられる場合があると説明されています。

ただし、WPFやWindows Formsで await 後にUI部品を更新する場合は、UIスレッドへ戻る必要があります。アプリケーションコードではむやみに ConfigureAwait(false) を付けず、ライブラリコードで使いどころを検討するのが基本です。

8-5. 無限待機を避けてタイムアウトを設定する

無限に待つコードは、障害時に処理が戻ってこない原因になります。

C#
await LongRunningOperationAsync(); // 失敗時に戻らない可能性がある

必要に応じてタイムアウトを設定します。

C#
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
await LongRunningOperationAsync(cts.Token);

または Task.WhenAny でタイムアウトを実装します。

C#
if (await Task.WhenAny(workTask, Task.Delay(10000)) != workTask)
{
throw new TimeoutException();
}

8-6. 例外処理を忘れずに書く

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

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

キャンセルを使う場合は、OperationCanceledException を想定します。

C#
try
{
await Task.Delay(10000, cancellationToken);
}
catch (OperationCanceledException)
{
Console.WriteLine("キャンセルされました");
}

Task.Wait を使うと例外が AggregateException に包まれる場合があるため、例外処理の面でも await のほうが扱いやすいです。

9. C#のwaitで発生しやすいトラブルと解決策

9-1. UIが固まる

原因は、UIスレッド上で Thread.SleepTask.Wait.Result を使っていることです。

悪い例です。

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

改善例です。

C#
private async void Button_Click(object sender, EventArgs e)
{
await Task.Delay(3000);
label1.Text = "完了";
}

UIイベントでは、async void が許容される代表的なケースです。ただし、通常のメソッドでは async Task を使いましょう。

9-2. 処理が戻ってこない

原因は、無限待機、キャンセル未対応、外部APIの応答待ちなどです。

改善策は、タイムアウトとキャンセルを設定することです。

C#
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
await CallApiAsync(cts.Token);

9-3. デッドロックが発生する

原因は、非同期処理を .Wait().Result で同期的に待っていることです。

悪い例です。

C#
var data = GetDataAsync().Result;

改善例です。

C#
var data = await GetDataAsync();

「async all the way」という考え方で、呼び出し元まで非同期にするのが基本です。

9-4. CPU使用率が高くなる

原因は、待機せずにループを回し続けるビジーウェイトです。

悪い例です。

C#
while (!IsReady())
{
// 何もしないがCPUを使い続ける
}

改善例です。

C#
while (!IsReady())
{
await Task.Delay(100);
}

さらに良いのは、イベントや通知で状態変化を受け取る設計です。

9-5. 待機時間が正確にならない

Thread.SleepTask.Delay は、リアルタイム処理のような厳密な時間保証には向いていません。Thread.Sleep の実際のタイムアウトは、システムクロックの解像度などの影響で指定値と完全に一致しない場合があります。

ゲーム、音声処理、制御系などで高精度なタイミングが必要な場合は、専用のタイマー、ゲームループ、リアルタイム性を考慮した設計を検討します。

9-6. 非同期処理の例外を捕捉できない

原因は、非同期処理を待たずに投げっぱなしにしていることです。

悪い例です。

C#
DoWorkAsync(); // awaitしていない

改善例です。

C#
await DoWorkAsync();

どうしても待たずに実行する場合は、例外を内部で捕捉する仕組みを用意します。

C#
_ = Task.Run(async () =>
{
try
{
await DoWorkAsync();
}
catch (Exception ex)
{
Log(ex);
}
});

9-7. テストが不安定になる

原因は、Task.DelayThread.Sleep に依存したテストです。

悪い例です。

C#
await Task.Delay(500);
Assert.Equal(1, result.Count);

改善策は、処理完了を明示的に待つことです。

C#
await processor.Completed;
Assert.Equal(1, result.Count);

待機が必要な場合でも、短い間隔で条件を確認し、全体にタイムアウトを設定するほうが安定します。

10. 用途別:C#のwaitの正しい選び方

10-1. コンソールアプリで待機したい場合

簡単なコンソールアプリでは、Thread.Sleep でも動作します。

C#
Thread.Sleep(1000);

ただし、非同期処理を使うなら async Task Main にして await Task.Delay を使うのがおすすめです。

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

10-2. WPF・Windows Formsで待機したい場合

UIアプリでは、Thread.SleepTask.Wait は避けます。

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

await Task.Delay(1000);

button1.Enabled = true;
}

UIを更新する必要があるため、通常は await 後にUIスレッドへ戻る挙動を利用します。

10-3. ASP.NET Coreで待機したい場合

ASP.NET Coreでは、リクエスト処理中に Thread.SleepTask.Wait でスレッドをブロックしないようにします。

C#
[HttpGet]
public async Task<IActionResult> Get(CancellationToken cancellationToken)
{
await Task.Delay(1000, cancellationToken);
return Ok("完了");
}

外部APIやDBアクセスも、非同期APIを使って await するのが基本です。

10-4. バッチ処理で待機したい場合

バッチ処理では、リトライや一定間隔実行で待機が必要になります。

C#
while (!stoppingToken.IsCancellationRequested)
{
await ExecuteBatchAsync(stoppingToken);
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
}

停止要求を受け取れるように、CancellationToken を渡す設計にしましょう。

10-5. ゲーム・リアルタイム処理で待機したい場合

ゲームやリアルタイム処理では、単純な Thread.SleepTask.Delay だけで正確なフレーム制御を行うのは難しいです。

多くの場合、ゲームエンジンの更新ループ、フレーム時間、専用タイマーを使います。Unityであれば WaitForSecondsasync ライブラリ、独自エンジンであればゲームループ内の経過時間計算を使うほうが自然です。

10-6. 単体テストで待機したい場合

単体テストでは、固定時間の待機をできるだけ避けます。

避けたい書き方です。

C#
Thread.Sleep(1000);

より良い書き方です。

C#
await target.OperationCompleted;

どうしても待つ場合は、タイムアウト付きで条件を待ちます。

C#
await WaitUntilAsync(() => target.IsReady, TimeSpan.FromSeconds(3));

11. C#のwaitに関するよくある質問

11-1. Thread.SleepとTask.Delayはどちらを使うべき?

基本は Task.Delay です。

非同期処理、UIアプリ、Webアプリでは await Task.Delay(...) を使います。Thread.Sleep は現在のスレッドを止めるため、UIフリーズやスレッドの無駄遣いにつながります。

ただし、コンソールアプリの簡単な検証や、明確にスレッドを止めたいケースでは Thread.Sleep を使うこともあります。

11-2. Task.Waitとawaitは何が違う?

Task.Wait は現在のスレッドをブロックしてタスク完了を待ちます。

await は非同期的に完了を待ち、待機中に現在のスレッドをブロックしません。非同期処理では、基本的に Task.Wait ではなく await を使います。

11-3. waitとawaitは同じ意味?

同じではありません。

wait は一般的な「待つ」という意味で使われることが多く、C#の予約語ではありません。

await はC#の非同期処理で使う演算子です。TaskTask<T> などの完了を非同期に待つために使います。

11-4. 1秒待つにはどう書けばいい?

非同期で1秒待つなら次のように書きます。

C#
await Task.Delay(1000);

または、読みやすく次のようにも書けます。

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

同期的にスレッドを止めるなら次の書き方です。

C#
Thread.Sleep(1000);

11-5. ミリ秒ではなく秒で指定できる?

できます。TimeSpan.FromSeconds を使います。

C#
await Task.Delay(TimeSpan.FromSeconds(5));
Thread.Sleep(TimeSpan.FromSeconds(5));

可読性を重視するなら、1000 のような数値を直接書くより TimeSpan.FromSeconds(1) のほうがおすすめです。

11-6. 非同期処理を待たずに実行する方法は?

次のように、あえて待たずに実行できます。

C#
_ = DoWorkAsync();

ただし、例外を捕捉できない、完了タイミングが分からない、アプリ終了時に処理が途中で止まる可能性がある、という問題があります。

安全に行うなら、内部で例外処理を行います。

C#
_ = Task.Run(async () =>
{
try
{
await DoWorkAsync();
}
catch (Exception ex)
{
Log(ex);
}
});

11-7. C#で処理完了まで待つにはどうする?

非同期処理の完了を待つなら、基本は await です。

C#
await DoWorkAsync();

複数の処理をすべて待つなら Task.WhenAll を使います。

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

どれか1つが完了するまで待つなら Task.WhenAny を使います。

C#
Task completed = await Task.WhenAny(task1, task2, task3);

同期的に待つ Task.WaitTask.WaitAll は、UIフリーズやデッドロックを招く可能性があるため、非同期コードでは原則として避けましょう。

まとめ

C#のwaitを理解するうえで重要なのは、「何を待つのか」と「スレッドをブロックしてよいのか」を分けて考えることです。

Thread.Sleep は、現在のスレッドを指定時間停止します。シンプルですが、UIアプリやWebアプリではフリーズやスレッドの無駄遣いにつながるため注意が必要です。

Task.Delay は、指定時間後に完了する Task を作ります。await Task.Delay(...) と書くことで、非同期に待機できます。現代のC#で「一定時間待つ」処理を書くなら、まずこの方法を選びます。

Task.Wait は、非同期処理を同期的に待つメソッドです。呼び出し元スレッドをブロックするため、UIフリーズやデッドロックの原因になりやすく、非同期コードでは基本的に await に置き換えるべきです。

使い分けの目安は次のとおりです。

目的推奨
1秒待つawait Task.Delay(1000)
非同期処理の完了を待つawait
複数Taskをすべて待つawait Task.WhenAll(...)
どれか1つのTaskを待つawait Task.WhenAny(...)
UIを固めずに待つawait Task.Delay(...)
スレッドを明示的に止めるThread.Sleep(...)
非同期処理を同期的に待つ原則避ける

C#で待機処理を書くときは、安易に Thread.SleepTask.Wait を使わず、まず async / awaitTask.Delay を検討しましょう。これだけで、UIフリーズ、デッドロック、スレッドブロック、テスト不安定化といった多くのトラブルを避けやすくなります。