非同期処理の例え話

2022年12月19日

朝食を作って食べる時を例に考えましょう

朝食の用意

ここでは、身近な例として朝食を作るときの料理手順を考えてみましょう

順番

  1. フライパンを熱し、卵を焼く
  2. コーヒーを淹れる
  3. 皿を並べる

イメージ

前の処理が終わると次の処理といったような継続処理を指します

同期処理(順次実行)では、1が終わったら2、2が終わったら3・・・と処理されます
卵を焼いている間、お皿は並べないのです

コード

まずは順番に実行する様子をコードにしてみましょう
この手順の方法を同期処理と呼びます

一番簡単なコードは次のようになります

Console.WriteLine("卵を焼く");
Console.WriteLine("コーヒーを淹れる");
Console.WriteLine("皿を並べる");

行は増えますが、クラス、メソッドに分解して構成しなおしてみましょう

Egg egg = new Egg();
egg.Fly();
Coffee coffee = new Coffee();
coffee.Pour();
Dish dish = new Dish();
dish.Arrange();

class Egg
{
    public void Fly()
    {
        Console.WriteLine("卵を焼く");
        Thread.Sleep(3000);
    }
}

class Coffee
{
    public void Pour()
    {
        Console.WriteLine("コーヒーを淹れる");
        Thread.Sleep(2000);
    }
}

class Dish
{
    public void Arrange()
    {
        Console.WriteLine("皿を並べる");
        Thread.Sleep(500);
    }
}

結果

実行結果を見てみましょう
イメージの通り、順番に実行されているのがわかります

朝食を並行して準備する

イメージ

準備を同時に実行するイメージです
卵を焼きながら、コーヒーを淹れ、お皿も用意します
実際はこのようにしますよね
一つが終わるまで、じっと待ったりしません

同期コードを非同期に変更して、確認してみましょう

まず、上記のコードを少し変更しておきます

朝食クラスにカスタマイズ

朝食クラスを作り、必要なコードをメソッド化します

Breakfast breakfast = new Breakfast();
breakfast.Cook();

class Breakfast
{
    public void Cook()
    {
        Egg egg = FlyEgg();
        egg.Fly();

        Coffee coffee = PourCoffee();
        coffee.Pour();

        Dish dish = ArrangeDish();
        dish.Arrange();

        Egg FlyEgg()
        {
            return new Egg();
        }

        Coffee PourCoffee()
        {
            return new Coffee();
        }

        Dish ArrangeDish()
        {
            return new Dish();
        }
    }
}

class Egg
{
    public void Fly()
    {
        Console.WriteLine("卵を焼く");
        Thread.Sleep(3000);
    }
}

class Coffee
{
    public void Pour()
    {
        Console.WriteLine("コーヒーを淹れる");
        Thread.Sleep(2000);
    }
}

class Dish
{
    public void Arrange()
    {
        Console.WriteLine("皿を並べる");
        Thread.Sleep(500);
    }
}

非同期コードに変更

同時実行できるように変更します
次は、Fly()メソッドの終了を待つコードです
この次の行に処理を移行するには、Fly()メソッドが終わらないといけません
ただし、待つ間は呼び出し元に制御が戻ります
システムがロックすることがないのが特徴です

 await egg.Fly();

次は、タスクの終了を待つコードです
タスクの終了を知ることにも使います
この次の行に処理を移すには、タスク一覧が全て終了していなければなりません

Task.WaitAll(タスク一覧)
Breakfast breakfast = new Breakfast();
breakfast.Cook();

class Breakfast
{
    public void Cook()
    {
        Task<Egg> eggTask = FlyEggAsync();
        Task<Coffee> coffeeTask = PourCoffeeAsync();
        Task<Dish> dishTask = ArrangeDishAsync();

        Task.WaitAll(new Task[] { eggTask, coffeeTask, dishTask });
        Console.WriteLine("作り終わり");
    }

    async Task<Egg> FlyEggAsync()
    {
        var egg = new Egg();
        await egg.Fly();
        return egg;
    }

    async Task<Coffee> PourCoffeeAsync()
    {
        var coffee = new Coffee();
        await coffee.Pour();
        return coffee;
    }

    async Task<Dish> ArrangeDishAsync()
    {
        var dish = new Dish();
        await dish.Arrange();
        return dish;
    }
}

class Egg
{
    public async Task<Egg> Fly()
    {
        Console.WriteLine("卵を焼き始める");
        await Task.Delay(3000);
        Console.WriteLine("卵を焼き終わり");
        return this;
    }
}

class Coffee
{
    public async Task<Coffee> Pour()
    {
        Console.WriteLine("コーヒーを淹れ始める");
        await Task.Delay(2000);
        Console.WriteLine("コーヒーを淹れ終わり");
        return this;
    }
}

class Dish
{
    public async Task<Dish> Arrange()
    {
        Console.WriteLine("皿を並べ始める");
        await Task.Delay(2000);
        Console.WriteLine("皿を並べ終わり");
        return this;
    }
}

結果

実行結果を見てみましょう
わかりやすくするためにスタートは3つの処理同時としています
終わる時間は分けていますので、順番に終了し、最後まで終わると「作り終わり」と表示されるのがわかりますね

まとめ

非同期処理はどのような時に必要でしょう

  • ネットワークから大きなデータをダウンロード中に他の処理をしたい場合
  • 複数のセンサーからの情報を同時に取得したい場合(各センサーからの受信に時間がかかるケース)

などがありますね

C#

Posted by hidepon