Task.Run() / Task.Delay()
| 対応: | C# 5.0(2012) |
|---|
バックグラウンドでタスクを実行したり、指定時間だけ待機したりする静的メソッドです。複数タスクを並列実行する『WhenAll()』『WhenAny()』も合わせて解説します。
構文
using System.Threading.Tasks; // バックグラウンドスレッドで処理を実行します。 Task task = Task.Run(() => action); Task<T> task = Task.Run(() => 戻り値のある処理); // 指定ミリ秒だけ非同期で待機します。 await Task.Delay(milliseconds); // すべてのタスクが完了するまで待機します。 await Task.WhenAll(task1, task2, task3); // 最初に完了したタスクだけを待機します。 Task first = await Task.WhenAny(task1, task2, task3);
メソッド一覧
| メソッド | 概要 |
|---|---|
| Task.Run(action) | スレッドプールでデリゲートを実行し、その完了を表す Task を返します。 |
| Task.Delay(ms) | 指定ミリ秒後に完了する Task を返します。スレッドをブロックしません。 |
| Task.WhenAll(tasks) | すべてのタスクが完了するまで非同期で待機します。 |
| Task.WhenAny(tasks) | 最初に完了したタスクを返します(タイムアウト実装に便利です)。 |
| Task.CompletedTask | 即完了済みの Task を返します(同期的に完了する場合の戻り値に使います)。 |
| task.Wait() | タスクが完了するまで現在のスレッドをブロックします。 |
| task.Result | タスクの結果を同期的に取得します(完了まで待機します)。 |
サンプルコード
Program.cs
using System;
using System.Threading.Tasks;
// Task.Run でバックグラウンド処理を実行します。
Task<int> bgTask = Task.Run(() =>
{
int sum = 0;
for (int i = 1; i <= 100; i++) sum += i;
return sum;
});
Console.WriteLine(await bgTask); // 5050
// Task.Delay で非同期にスリープします(Thread.Sleep の非同期版)。
Console.WriteLine("処理開始");
await Task.Delay(1000); // 1 秒待機します。
Console.WriteLine("1 秒後");
// WhenAll: 複数のタスクを並列実行してすべての完了を待ちます。
Task<string> t1 = Task.Run(async () => { await Task.Delay(100); return "タスク1"; });
Task<string> t2 = Task.Run(async () => { await Task.Delay(200); return "タスク2"; });
Task<string> t3 = Task.Run(async () => { await Task.Delay(50); return "タスク3"; });
string[] results = await Task.WhenAll(t1, t2, t3);
Console.WriteLine(string.Join(", ", results)); // タスク1, タスク2, タスク3
// WhenAny: 最初に完了したタスクを取得します(タイムアウト実装例)。
Task<int> longTask = Task.Run(async () => { await Task.Delay(5000); return 42; });
Task timeoutTask = Task.Delay(1000);
Task finished = await Task.WhenAny(longTask, timeoutTask);
if (finished == timeoutTask)
Console.WriteLine("タイムアウト");
else
Console.WriteLine($"完了: {longTask.Result}");
dotnet script task_run_delay.csx 5050 処理開始 1 秒後 タスク1, タスク2, タスク3 タイムアウト
実践パターン: 並列ダウンロードと集計
複数のデータを並列取得して集計するパターンです。逐次 await より大幅に時間を短縮できます。
ParallelFetch.cs
using System;
using System.Diagnostics;
using System.Threading.Tasks;
// スコアを取得する非同期メソッドです(実際には DB や API を叩きます)。
static async Task<int> FetchScoreAsync(string name, int delayMs)
{
await Task.Delay(delayMs);
// 名前に基づいてスコアを返します。
return name.Length * 100 + delayMs / 10;
}
var sw = Stopwatch.StartNew();
// 逐次実行: 合計 1500ms かかります。
sw.Restart();
int s1 = await FetchScoreAsync("Shinji Ikari", 500);
int s2 = await FetchScoreAsync("Rei Ayanami", 500);
int s3 = await FetchScoreAsync("Asuka Langley", 500);
Console.WriteLine($"逐次: {s1 + s2 + s3}点 / {sw.ElapsedMilliseconds}ms");
// 並列実行: 最長の 500ms で完了します。
sw.Restart();
Task<int> p1 = FetchScoreAsync("Shinji Ikari", 500);
Task<int> p2 = FetchScoreAsync("Rei Ayanami", 500);
Task<int> p3 = FetchScoreAsync("Asuka Langley", 500);
int[] parallelResults = await Task.WhenAll(p1, p2, p3);
int total = parallelResults[0] + parallelResults[1] + parallelResults[2];
Console.WriteLine($"並列: {total}点 / {sw.ElapsedMilliseconds}ms");
dotnet script parallel_fetch.csx 逐次: 3770点 / 1510ms 並列: 3770点 / 512ms
実践パターン: WhenAny でタイムアウトを実装
WhenAny と Task.Delay を組み合わせてタイムアウト処理を実装するパターンです。CancellationToken が使えない場合に有効です。
TaskTimeout.cs
using System;
using System.Threading;
using System.Threading.Tasks;
// タイムアウト付きで非同期処理を実行します。
static async Task<T?> WithTimeoutAsync<T>(Task<T> task, int timeoutMs)
{
Task timeoutTask = Task.Delay(timeoutMs);
Task finished = await Task.WhenAny(task, timeoutTask);
if (finished == timeoutTask)
{
Console.WriteLine($"タイムアウト({timeoutMs}ms)");
return default;
}
return await task;
}
// 高速タスク(200ms)
Task<string> fastTask = Task.Run(async () =>
{
await Task.Delay(200);
return "Kaworu Nagisa";
});
// 低速タスク(2000ms)
Task<string> slowTask = Task.Run(async () =>
{
await Task.Delay(2000);
return "遅延データ";
});
string? fastResult = await WithTimeoutAsync(fastTask, 500);
Console.WriteLine($"高速タスク: {fastResult ?? "null"}");
string? slowResult = await WithTimeoutAsync(slowTask, 500);
Console.WriteLine($"低速タスク: {slowResult ?? "null"}");
dotnet script task_timeout.csx 高速タスク: Kaworu Nagisa タイムアウト(500ms) 低速タスク: null
よくあるミス
よくあるミス1: task.Result / task.Wait() でデッドロック
非同期メソッド内で task.Result や task.Wait() を使うとデッドロックが発生することがあります。ASP.NET や UI スレッドがある環境では特に危険です。
using System;
using System.Threading.Tasks;
static async Task<int> GetDataAsync()
{
await Task.Delay(100);
return 42;
}
// NG: .Result は非同期メソッド内で使うとデッドロックの原因になります。
async Task Main()
{
int result = GetDataAsync().Result; // デッドロックの危険があります。
Console.WriteLine(result);
}
修正後は次の通りです。
using System;
using System.Threading.Tasks;
static async Task<int> GetDataAsync()
{
await Task.Delay(100);
return 42;
}
// OK: 常に await を使います。
async Task Main()
{
int result = await GetDataAsync(); // await を使います。
Console.WriteLine(result); // 42
}
よくあるミス2: WhenAll 内の例外が一部しかキャッチされない
WhenAll に複数の失敗タスクがある場合、await すると最初の例外のみ catch されます。すべての例外を確認するには task.Exception を参照します。
using System;
using System.Threading.Tasks;
Task<int> fail1 = Task.Run<int>(() => throw new Exception("エラー1"));
Task<int> fail2 = Task.Run<int>(() => throw new Exception("エラー2"));
Task<int> whenAll = Task.WhenAll(fail1, fail2);
try
{
await whenAll;
}
catch (Exception ex)
{
// await では最初の例外のみ捕捉されます。
Console.WriteLine($"catch: {ex.Message}");
// 全例外を確認するには Exception.InnerExceptions を参照します。
if (whenAll.Exception != null)
{
foreach (Exception inner in whenAll.Exception.InnerExceptions)
Console.WriteLine($" 内部例外: {inner.Message}");
}
}
dotnet run catch: エラー1 内部例外: エラー1 内部例外: エラー2
概要
『Task.WhenAll()』は複数の非同期処理を並列で走らせ、すべての完了を待ちます。逐次的に await を重ねるより合計待機時間を短縮できます。
『task.Result』や『task.Wait()』を非同期メソッド内で使うとデッドロックが発生することがあります。非同期コードの中では必ず『await』を使ってください。非同期の基本構文は『async / await』を、非同期処理でのキャンセルは『CancellationToken』を参照してください。
記事の間違いや著作権の侵害等ございましたらお手数ですがこちらまでご連絡頂ければ幸いです。