ユニファ開発者ブログ

ユニファ株式会社プロダクトデベロップメント本部メンバーによるブログです。

すき焼き鍋からタスク並列処理

この記事はユニファAdvent Calender 2020の17日目の記事です。

こんにちは、プロダクトエンジニアリング部のちょうです。最近天気は段々寒くなってきましたね。こんな時期欠かせない料理といえば鍋でしょう。鍋は煮ながら食べる料理ですゆえ、準備の手間はかからないし、好きな食材をどんどんいれることができます。

どころで、最近どれぐらい早く鍋をできるのを考えていました。例えば、炊飯器を15分でご飯を炊けるとする。その15分で鍋の食材を何も処理していない状態からホカホカの鍋にできるのでしょうか。

ここで、この鍋を人気のすき焼き鍋にしましょう。すきやき鍋は必要な食材はこちらです。

レシピから抜粋

  • 牛ロース肉(薄切り)
  • ねぎ
  • しらたき
  • えのきたけ
  • 焼き豆腐
  • しいたけ
  • 白菜
  • サラダ油

割り下については、市販のすき焼きのたれを代用。

ざっくりな料理の流れは

  1. 具材の下処理
  2. サラダ油でねぎと牛肉を炒めて、すきやきのたれを回しかける
  3. 鍋に具材を入れて煮込み

家に二口のコンロはあれば、炒めるのと鍋の煮込みを同時にできます。そして下処理も同時にします。フライパンが熱くなるまではちょっと時間がいります。鍋に水とすきやきのたれを入れて沸騰するまでは5分ぐらいかかります。なので

  • 鍋に水とすきやきのたれを入れて煮る
  • ねぎを下処理する
  • フライパンにサラダ油を入れて中火にする
  • 牛ロース肉を下処理する
  • フライパンが熱くなったらネギをフライパンにいれる
  • ネギに焦げ目がちょっとでたら、牛ロース肉をフライパンいれる
  • のこり時間でほかの具材を下処理する(硬いから柔らかいもの順)
  • 硬いものから柔らかいもの順で鍋にいれる

が一番効率でしょう。

すきやき鍋料理の流れ
すきやき鍋料理の流れ

この料理の流れを技術的な視点からみると、コンロ2口プラス料理する人で実質三プロセッサーです!言い方はちょっと変かもしれませんが(料理する人はコンロではない)、同時に処理することでスピードが上がったのはたしかです。

並列処理の中で、これはタスク並列処理と分類されると思います。もうひとつ、データ並列処理がありますが、それはすべてのプロセッサーはが別々のデータに同時に同じ処理を行うというタイプです。

タスク並列処理では、各タスクの間依頼関係があると、その前後関係が決められます。逆に依頼関係がないと、タスクは同時に行うことができます。例えば、フライパンでネギと牛肉を炒めるのは、先にネギをいれるのです。牛肉はあとなので、 ネギ > 牛肉 という関係があります。そして、フライパンが熱くなるまでネギをいれないので、 フライパンが熱くなる > ネギ という関係があります。最後、ネギを下処理しないと、フライパンにいれないので、 下処理前のネギ > ネギ という関係があります(同じく 下処理前の牛肉 > 牛肉 )。まとめると

  • ネギ > 牛肉
  • フライパンが熱くなる > ネギ
  • 下処理前のネギ > ネギ
  • 下処理前の牛肉 > 牛肉

並列処理では、この関係を満たされば問題ないです。一つの答えは

料理する人 コンロ
ネギを下処理する
牛肉を下処理する サラダ油を入れ、中火
ネギをいれる
牛肉をいれる

タスク並列処理ではどれぐらい早くなるのは、依頼関係の洗い出し及びプロセスの設計が重要です。

ネギの下処理とコンロの中火を一緒やってしまうと、下処理に時間がかかって、コンロに警告が出て自動的に止まることもあります。安全のために、ネギを先に処理するのがおすすめです。こういうタスク間で直接ではない関係性も含めて考慮しましょう。

ではプログラムでシミュレーションしましょう。

import org.junit.jupiter.api.Test
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit

class SukiyakiTest {

    class StoveCell() : Cell() {
        private var waitingForBeef = false

        override fun receive(context: CellContext, event: Event) {
            when (event) {
                TurnOnStoveEvent -> context.schedule(500, TimeUnit.MILLISECONDS, FryingPanReadyEvent)
                FryingPanReadyEvent -> context.parent.tell(event)
                AddLeekToFryingPanEvent -> {
                    waitingForBeef = true
                    context.schedule(2000, TimeUnit.MILLISECONDS, WaitingForBeefEvent)
                }
                AddBeefToFryingPanEvent -> {
                    if (waitingForBeef) {
                        waitingForBeef = false
                    }
                    context.schedule(4000, TimeUnit.MILLISECONDS, DoneEvent)
                }
                WaitingForBeefEvent -> if (waitingForBeef) {
                    context.logger.info("牛肉を待つ")
                }
                DoneEvent -> context.parent.tell(event)
                PoisonPill -> context.stopSelf()
            }
        }
    }

    class ChefCell(private val latch: CountDownLatch) : Cell() {
        private var _stove: CellRef? = null

        private val stove: CellRef
            get() = _stove!!

        override fun start(context: CellContext) {
            _stove = context.startChild(StoveCell())
            context.logger.info("ネギを下処理する")
            context.schedule(1000, TimeUnit.MILLISECONDS, TurnOnStoveEvent)
        }

        override fun receive(context: CellContext, event: Event) {
            when (event) {
                TurnOnStoveEvent -> {
                    stove.tell(TurnOnStoveEvent)
                    context.logger.info("フライパンにサラダ油をいれ、中火")
                    context.logger.info("牛肉を下処理する")
                    context.schedule(2000, TimeUnit.MILLISECONDS, AddBeefToFryingPanEvent)
                }
                FryingPanReadyEvent -> {
                    context.logger.info("フライパンにネギをいれる")
                    stove.tell(AddLeekToFryingPanEvent)
                }
                AddBeefToFryingPanEvent -> {
                    context.logger.info("フライパンに牛肉をいれる")
                    stove.tell(event)
                }
                DoneEvent -> {
                    context.logger.info("できあがり")
                    stove.tell(PoisonPill) // to stop stove
                    latch.countDown()
                }
            }
        }
    }

    object TurnOnStoveEvent : Event
    object FryingPanReadyEvent : Event
    object AddLeekToFryingPanEvent : Event
    object WaitingForBeefEvent : Event
    object AddBeefToFryingPanEvent : Event
    object DoneEvent : Event

    @Test
    fun test() {
        val latch = CountDownLatch(1)
        val system = CellSystem()
        system.add(ChefCell(latch))
        system.start()
        latch.await()
        system.stop()
    }
}

CellSystemが私が作ったActorモデルのスケジューラーシステムです。Actorモデル自体もタスク並列処理にピッタリするモデルです。コードの中各ステップの時間:

  • ネギ処理 1
  • 牛肉処理 2
  • フライパン熱くなるまで 1
  • ネギを入れてから牛肉を入れるまで 2
  • 牛肉をいれてからできあがり 4

単位は秒ですが、実質は10秒20秒単位と思ってください。

結果

2020-11-30 16:42:32.888 [worker-0] INFO  cell://ChefCell - ネギを下処理する
2020-11-30 16:42:33.897 [worker-0] INFO  cell://ChefCell - フライパンにサラダ油をいれ、中火
2020-11-30 16:42:33.897 [worker-0] INFO  cell://ChefCell - 牛肉を下処理する
2020-11-30 16:42:34.400 [worker-1] INFO  cell://ChefCell - フライパンにネギをいれる
2020-11-30 16:42:35.902 [worker-1] INFO  cell://ChefCell - フライパンに牛肉をいれる
2020-11-30 16:42:39.908 [worker-1] INFO  cell://ChefCell - できあがり

ここでworker-0は料理をする人です。worker-1はコンロです。

いかがでしょうか。普段の生活からも並列処理に関する知識も潜んでいますね。並列処理が人間の生活をモデルにしたといっても過言ではありませんね。時間があったら、ぜひまわりのことをタスク並列処理で考えてみてください。

最後に、ユニファが一緒に働いてくれるメンバーを募集しています。興味がある方ぜひ見てください。

採用情報 - ユニファ株式会社