基礎から学ぶC#入門 90日コース | 実践力を高める - Day 64:非同期処理実践

Web APP 90日で身につけるC#
スポンサーリンク
スポンサーリンク

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#

この場合、流れはこうなります。

  1. 処理A(3秒)を待つ
  2. 処理B(2秒)を待つ
  3. 処理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 が全部終わるのを待つ」

ためのメソッドです。

  • 引数に TaskTask<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 呼び出しに応用してみる

といった小さなステップを重ねながら、 非同期処理を「自分のアプリを気持ちよく動かすための技」として 少しずつ体に馴染ませていっていただければうれしいです。

タイトルとURLをコピーしました