Day 64 のゴールと全体像
Day 64 では、昨日学んだ async / await / Task を、もう一歩実践寄りに踏み込んでいきます。 テーマは次の3つです。
- 時間のかかる処理
- 複数タスク
- Task.WhenAll
「非同期処理って、実際のアプリでどう役立つの?」という疑問に、 手触りのあるコードで答えていく回だと思ってもらえるとちょうどいいです。
今日は、
- 時間のかかる処理を「疑似的に」作ってみて
- それを複数同時に走らせてみて
Task.WhenAllで「全部終わるのを待つ」
という流れを、ステップバイステップで追いかけていきます。
時間のかかる処理をイメージする
「待ち時間のある処理」の代表例
現実のアプリでは、次のような処理が「時間のかかる処理」になりやすいです。
- Web API にアクセスして、ネットワーク越しにデータを取得する
- 大きなファイルを読み込む・書き込む
- 複雑な計算を行う(画像処理・暗号化・統計処理など)
これらは、
- 「すぐには終わらない」
- 「でも、終わったら結果が欲しい」
という性質を持っています。
非同期処理は、こうした「待ち時間のある処理」を、 アプリ全体を止めずに扱うための仕組みです。
練習用に「疑似的な時間のかかる処理」を作る
実際のネットワークやファイルを使う前に、 まずは Task.Delay を使って「時間のかかる処理の練習台」を作ってみます。
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
Console.WriteLine("時間のかかる処理のテストを開始します。");
string result = await LongOperationAsync("処理A", 3000);
Console.WriteLine($"結果: {result}");
Console.WriteLine("テストが完了しました。");
}
// 疑似的な「時間のかかる処理」
static async Task<string> LongOperationAsync(string name, int milliseconds)
{
Console.WriteLine($"{name} を開始します({milliseconds}ミリ秒)…");
// 指定した時間だけ待つ(ここが「時間のかかる部分」のイメージ)
await Task.Delay(milliseconds);
Console.WriteLine($"{name} が完了しました。");
return $"{name}の結果";
}
}
C#このコードでは、
LongOperationAsyncが「時間のかかる処理」を表す非同期メソッドTask.Delay(milliseconds)が「待ち時間」を表現している部分
になっています。
複数タスクを同時に走らせてみる
まずは「順番に待つ」パターン
先ほどの LongOperationAsync を、 複数回呼び出してみましょう。
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
Console.WriteLine("順番に処理を実行します。");
string resultA = await LongOperationAsync("処理A", 3000);
Console.WriteLine($"処理Aの結果: {resultA}");
string resultB = await LongOperationAsync("処理B", 2000);
Console.WriteLine($"処理Bの結果: {resultB}");
string resultC = await LongOperationAsync("処理C", 1000);
Console.WriteLine($"処理Cの結果: {resultC}");
Console.WriteLine("すべての処理が完了しました。");
}
static async Task<string> LongOperationAsync(string name, int milliseconds)
{
Console.WriteLine($"{name} を開始します({milliseconds}ミリ秒)…");
await Task.Delay(milliseconds);
Console.WriteLine($"{name} が完了しました。");
return $"{name}の結果";
}
}
C#この場合、流れはこうなります。
- 処理A(3秒)を待つ
- 処理B(2秒)を待つ
- 処理C(1秒)を待つ
合計で 3 + 2 + 1 = 6秒 かかります。
これは「同期的に順番に待っている」イメージに近いです。
「同時に走らせて、全部終わるのを待つ」パターン
ここからが、非同期処理の本領発揮です。 同じ LongOperationAsync を、同時に走らせてみることを考えます。
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
Console.WriteLine("複数の処理を同時に開始します。");
// まず「タスク」を作る(まだ await しない)
Task<string> taskA = LongOperationAsync("処理A", 3000);
Task<string> taskB = LongOperationAsync("処理B", 2000);
Task<string> taskC = LongOperationAsync("処理C", 1000);
Console.WriteLine("すべての処理を開始しました。あとは完了を待つだけです。");
// ここで「全部終わるのを待つ」
string[] results = await Task.WhenAll(taskA, taskB, taskC);
Console.WriteLine("=== 結果一覧 ===");
Console.WriteLine($"処理Aの結果: {results[0]}");
Console.WriteLine($"処理Bの結果: {results[1]}");
Console.WriteLine($"処理Cの結果: {results[2]}");
Console.WriteLine("すべての処理が完了しました。");
}
static async Task<string> LongOperationAsync(string name, int milliseconds)
{
Console.WriteLine($"{name} を開始します({milliseconds}ミリ秒)…");
await Task.Delay(milliseconds);
Console.WriteLine($"{name} が完了しました。");
return $"{name}の結果";
}
}
C#ここでのポイントは、
LongOperationAsyncを呼び出したときに、すぐにはawaitしない- 代わりに、
Task<string>として受け取っておく Task.WhenAll(taskA, taskB, taskC)で「全部終わるのを待つ」
という流れになっていることです。
この場合、
- 処理A(3秒)
- 処理B(2秒)
- 処理C(1秒)
が ほぼ同時にスタートし、 全体の時間は「いちばん長い処理(3秒)」に近づきます。
つまり、6秒かかっていたものが、約3秒で終わるイメージです。
Task.WhenAll の役割を深掘りする
Task.WhenAll とは
Task.WhenAll は、
「複数の Task が全部終わるのを待つ」
ためのメソッドです。
- 引数に
TaskやTask<T>を複数渡す - それらがすべて完了したタイミングで、ひとつの
Taskとして完了する
という動きをします。
戻り値の形
Task.WhenAll(Task t1, Task t2, ...)- 戻り値は
Task(結果なし)
- 戻り値は
Task.WhenAll(Task<T> t1, Task<T> t2, ...)- 戻り値は
Task<T[]>(結果の配列)
- 戻り値は
先ほどの例では、
string[] results = await Task.WhenAll(taskA, taskB, taskC);
C#と書いていました。 これは、
taskA,taskB,taskCがすべて完了したら- それぞれの結果を
string[]にまとめて返す
という意味になります。
Task.WhenAll のイメージ
Task.WhenAll は、
「3人に別々の仕事をお願いして、全員が終わるのを待ってから次のステップに進む」
ようなイメージです。
- 仕事A:3秒かかる
- 仕事B:2秒かかる
- 仕事C:1秒かかる
というとき、
- 全員に同時に「お願いします」と声をかけて
- 「全員終わった?」と確認するのが
Task.WhenAll
という感じです。
実践的な例:疑似的な「ファイルダウンロード」
もう少し実践寄りのイメージとして、 「複数のファイルをダウンロードする」ようなコードを考えてみます。 ここでは、実際のネットワークではなく Task.Delay で代用します。
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
Console.WriteLine("複数ファイルのダウンロードを開始します。");
// ダウンロードタスクをまとめて作る
Task<string> file1 = DownloadFileAsync("file1.zip", 2000);
Task<string> file2 = DownloadFileAsync("file2.zip", 3000);
Task<string> file3 = DownloadFileAsync("file3.zip", 1500);
Console.WriteLine("すべてのダウンロードを開始しました。");
// すべてのダウンロードが終わるのを待つ
string[] results = await Task.WhenAll(file1, file2, file3);
Console.WriteLine("=== ダウンロード結果 ===");
foreach (var r in results)
{
Console.WriteLine(r);
}
Console.WriteLine("すべてのファイルのダウンロードが完了しました。");
}
// 疑似的なファイルダウンロード
static async Task<string> DownloadFileAsync(string fileName, int milliseconds)
{
Console.WriteLine($"{fileName} のダウンロードを開始します…");
// ダウンロードに時間がかかるイメージ
await Task.Delay(milliseconds);
Console.WriteLine($"{fileName} のダウンロードが完了しました。");
return $"{fileName} のダウンロード成功";
}
}
C#このコードでは、
DownloadFileAsyncが「ファイルダウンロード」を表す非同期メソッドTask.WhenAllで「すべてのファイルのダウンロード完了」を待っている
という構造になっています。
実際のアプリでは、
Task.Delayの代わりにHttpClientで Web API を呼ぶ- ファイルの読み書きに
FileStreamの非同期メソッドを使う
といった形で、 「本物の時間のかかる処理」に置き換えていくことができます。
複数タスクを扱うときの注意ポイント
1. 例外が発生したとき
Task.WhenAll を使っているときに、 どれかのタスクで例外が発生すると、
Task.WhenAll自体が例外を投げます
そのため、
try
{
string[] results = await Task.WhenAll(taskA, taskB, taskC);
}
catch (Exception ex)
{
Console.WriteLine($"エラーが発生しました: {ex.Message}");
}
C#のように、 try / catch で囲んでおくと安心です。
2. 「同時にやるべきか」「順番にやるべきか」
すべての処理を同時に走らせればいい、というわけではありません。
- データベースの更新など、「順番が大事な処理」
- 外部サービスへのアクセス回数に制限がある場合
などでは、あえて「順番に await する」方が安全なこともあります。
非同期処理は「何でもかんでも並列にする魔法」ではなく、 「待ち時間をうまく活用するための道具」だという感覚が大事です。
Day 64 用ミニテンプレート
最後に、今日の内容をそのまま使える形のテンプレートとしてまとめておきます。
複数タスクを同時に開始して、全部終わるのを待つ基本形
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
Console.WriteLine("複数タスクの実行を開始します。");
// タスクを作成(まだ await しない)
Task<string> task1 = DoWorkAsync("タスク1", 1000);
Task<string> task2 = DoWorkAsync("タスク2", 2000);
Task<string> task3 = DoWorkAsync("タスク3", 1500);
Console.WriteLine("すべてのタスクを開始しました。");
// すべてのタスクが終わるのを待つ
string[] results = await Task.WhenAll(task1, task2, task3);
Console.WriteLine("=== 結果 ===");
foreach (var r in results)
{
Console.WriteLine(r);
}
Console.WriteLine("すべてのタスクが完了しました。");
}
static async Task<string> DoWorkAsync(string name, int milliseconds)
{
Console.WriteLine($"{name} を開始します({milliseconds}ミリ秒)…");
await Task.Delay(milliseconds);
Console.WriteLine($"{name} が完了しました。");
return $"{name}の結果";
}
}
C#Day 64 のまとめ
Day 64 では、
Task.Delayを使って「時間のかかる処理」のイメージをつかみ- 複数の非同期メソッドを「順番に待つ」パターンと
- 「同時に開始して、全部終わるのを待つ」パターンを比べ
Task.WhenAllの役割(複数タスクの完了待ち)を深掘りしました。
非同期処理は、
- ひとつひとつの処理を
awaitするだけでも便利ですが - 複数タスクをうまく組み合わせることで、 アプリ全体の「待ち時間の使い方」がぐっと洗練されていきます。
ぜひ、
- 疑似的な「時間のかかる処理」をいくつか作ってみる
Task.WhenAllを使って「全部終わるのを待つ」コードを書いてみる- 実際のファイル処理や Web API 呼び出しに応用してみる
といった小さなステップを重ねながら、 非同期処理を「自分のアプリを気持ちよく動かすための技」として 少しずつ体に馴染ませていっていただければうれしいです。
