R

ReflexKR

Reflex公式ドキュメント

深夜厨房の並行処理

·

講座の概要

1

初日の行列:並行と並列

2

料理人を増やす:threadingの基本

3

黒板の数が合わない:競合状態とLock

4

コンロは3つだけ:同期の道具

5

包丁とまな板:デッドロック

6

伝票差し:queue

7

出張料理人:concurrent.futures

8

生地はなぜ速くならない:GILと並列

9

一人で回すホール係:asyncioの第一歩

10

団体予約のキャンセル:構造化された並行処理

11

止まったホール:ブロッキングと組み合わせ

12

評論家の夜:総まとめ

深夜厨房の並行処理
12限目

時間 9:10–10:00

評論家の夜:総まとめ

今夜の注文書

#12 · 十二日目の夜

評論家の夜

ピンチ

窓際の客が名刺を差し出す。美食評論家。今夜、12皿のコースを正式に注文する。3・7・11皿目は、最初の1回だけやけに時間がかかる。

目標

12皿をすべて出す。ただしコンロは3つまで、3秒以内に終える。

深夜食堂 ドンシ

金曜の夜10時。約束どおり、窓際の客が入ってきます。いつもひとりで、いつも麺を一杯、いつも腕時計と小さな手帳。今夜は席に着くなり、名刺を1枚テーブルに置きます。美食評論家。

「ここ何週間か、この厨房を見てきました。今日はちゃんと注文します。12皿のコースで」 評論家は手帳を開き、腕時計を外します。

厨房が静まり返ります。ミンホは包丁を落とし、スアは伝票差しを整え直し、タクさんは黙ってコンロを磨きます。ルーが注文書を持って入ってきます。ユン・ソヨンシェフが首から下げたタイマーを0に戻します。「厨房はプログラムだ。今夜は、これまで作ってきたものを全部動かすよ」

12-1. どの道具を出すか (8分)

11回の夜で、道具はずいぶんたまりました。お客さんが入ってきたとき、最初に問うべきことはひとつです。「この仕事は待つ仕事か、手がふさがる仕事か?」

状況出す道具厨房でいえば限目
待つ仕事がとても多い(接続が数百〜数万)asyncio (gather, TaskGroup)一人でホールを回すルー9〜10
待つ仕事だが同期ライブラリしかない、数十件threading, ThreadPoolExecutor複数の料理人、出張料理人2, 7
計算が多い(CPU)ProcessPoolExecutor, multiprocessing(または3.14tのスレッド)支店の厨房8
asyncioの中の古いブロッキングコードasyncio.to_threadオーブンの前に別に立つ人11
asyncioの中の重い計算loop.run_in_executor(プロセスプール, ...)支店に生地を任せる11
複数人がひとつの値を書き換えるLock、またはキューで持ち主をひとりに黒板の前のロック、伝票差し3, 6
同時にn個までSemaphore(n)コンロ3つ4, 11
一緒に終わらなければならず、時間を超えてもいけないTaskGroup, asyncio.timeout団体予約10

選ぶ順番はこうです。

  1. 並行処理は本当に必要か? 順番にやっても十分速いなら、そのままにします。1限目で見たとおり、並行処理はタダではありません。
  2. 待つ仕事か、計算か? 待つ仕事ならasyncioかスレッド、計算ならプロセス(または3.14t)です。GILのあるビルドでは、計算をスレッドで分けても速くなりません(8限目)。
  3. すでに使っているライブラリはasyncに対応しているか? 対応していればasyncioで、そうでなければスレッドで始めるか、asyncioの中で to_thread で包みます。
  4. 何を共有するか? 共有状態があればロックかキュー、資源の数に限りがあればセマフォ、一緒に終わる必要があればTaskGroupを足します。

12-2. 評論家のコース処理係 (14分)

評論家の注文書はこうです。皿は12、1皿に0.5秒。ところが3・7・11皿目は、最初の1回は材料がまだ戻りきっていなくて3倍(1.5秒)かかります。やり直せば0.5秒でできます。コンロは3つだけです。

必要な部品は3つです。コンロの数を守るセマフォ、1皿がだらだら長引かないようにする制限時間、途中で切られた皿をやり直す再試行。これをTaskGroupひとつに収めます。

import asyncio, time

SLOW = {3, 7, 11}                      # 最初の1回は3倍かかる皿

async def plate(n, attempt):
    await asyncio.sleep(0.5 * (3 if n in SLOW and attempt == 0 else 1))

async def order(n, stoves, limit_s, retries):
    for attempt in range(retries + 1):
        async with stoves:             # コンロを確保しているあいだだけ料理する
            try:
                async with asyncio.timeout(limit_s):
                    await plate(n, attempt)
                return True
            except TimeoutError:
                pass                   # コンロを手放し、列の最後に並び直してもう一度
    return False

async def service(stoves_n, limit_s, retries):
    stoves = asyncio.Semaphore(stoves_n)
    start = time.perf_counter()
    async with asyncio.TaskGroup() as tg:
        tasks = [tg.create_task(order(n, stoves, limit_s, retries)) for n in range(1, 13)]
    served = sum(t.result() for t in tasks)
    print(f"stoves={stoves_n} limit={limit_s} retries={retries}: "
          f"served {served}/12, {time.perf_counter() - start:.1f}s")

asyncio.run(service(3, None, 0))       # 制限時間なし
asyncio.run(service(3, 0.6, 0))        # 制限だけ
asyncio.run(service(3, 0.6, 1))        # 制限 + 再試行1回
# stoves=3 limit=None retries=0: served 12/12, 3.5s
# stoves=3 limit=0.6 retries=0: served 9/12, 2.2s
# stoves=3 limit=0.6 retries=1: served 12/12, 2.7s
# (かかる時間は実行のたびに0.1秒ほど変わる。1行目は3.5sか3.6s)
  • asyncio.timeout(None) は制限なしです。だから同じ関数で「制限なし」と「0.6秒制限」を両方回せます。
  • 制限がないと、遅い3皿がそれぞれ1.5秒ずつコンロをふさぎ、12皿すべてが出るまで3.5秒あまりかかります。
  • 制限だけをかけると、遅い皿は0.6秒で切られ、コンロが早く空きます。その代わり、その3皿は永遠に出せません。
  • 制限と再試行を一緒にかけると、切られた皿はコンロを手放して列の最後に並び直します。2回目は0.5秒で終わり、12皿すべてが出ます。

設計で押さえておきたい点が2つあります。

  • 再試行するときはコンロを手放します。 async with stoves が for の中にあるので、切られた皿はコンロを解放してから並び直します。コンロを握ったまま再試行すると、そのあいだほかの皿が遊んでしまいます。
  • 失敗を例外として送出せず、False で返します。 TaskGroupで例外が起きると、残りがすべてキャンセルされます(10限目)。団体予約ならそれが正解ですが、コースは1皿失敗しても残りは出さなければなりません。同じ理由で、asyncio.gather で集めてもかまいません。

実務では、再試行のあいだに await asyncio.sleep(0.05 * 2 ** attempt) のように少しずつ長く休むことがよくあります。全員が同時にまた押し寄せるのを防ぐためです(5限目のライブロックと同じ理由)。

下のタイムラインは、サーバーで同じ処理係を動かします。1行が1皿で、制限時間で切られた試行は灰色、再試行には r1、r2 が付きます。初期設定は、わざと目標を達成できないようにしてあります。

厨房タイムライン

注文

6

コンロ

2

制限時間(秒)

オフ

再試行

0

調理

待ち

キャンセル

失敗

評論家の12皿。コンロ、制限時間、再試行を変えて「提供 12 · 失敗 0、コンロ3以下、3秒以内」を達成してみてください。初期設定では1皿も出せません。
  • 初期設定(コンロ2、制限時間0.4秒、再試行0)で回すと、約2.4秒で提供 0 · 失敗 12になります。ふつうの皿でも0.5秒かかるのに制限が0.4秒なので、すべての皿が切られます。
  • コンロを3に増やして再試行だけ2に上げると、もっと悪くなります。同じ0.4秒の制限で3回切られるだけなので相変わらず提供0で、時間だけが約4.9秒に延びます。再試行は、制限時間の設定ミスを直してはくれません。制限をふつうの皿と同じ0.5秒に合わせても、ぎりぎりで切られて全部失敗します。
  • 制限時間をオフにすると全部提供されますが、コンロ3でも3.5秒あまりかかります。コンロを4に増やしても約3.0秒でぎりぎりですし、何よりコンロは3つしかありません。
  • 画面は実験を5秒あまりまでしか待ちません。コンロ2に再試行2のように時間のかかる設定は途中で切れて見えることがあるので、設定を変えて回し直してください。

シェフのひとこと

制限時間は「ふつうの皿よりは長く、しくじった皿よりは短く」。ふつうの皿まで切っちゃったら、再試行をいくら付けても無駄。数字を決める前に、まずふつうにかかる時間を測って。

12-3. ひとつの厨房に全部 (8分)

評論家のコースには、生地(計算)、古いオーブンのパン(ブロッキング)、コンロ料理(待ち)が一緒に入っています。それぞれを向いている人に任せ、ホールのルーが全部を待ちます。プロセスを使うので、ファイルに保存して実行します。

import asyncio, threading, time
from concurrent.futures import ProcessPoolExecutor

def knead(n):                          # 生地:手がふさがる仕事 → 支店の厨房(プロセス)
    return sum(i * i for i in range(n)) % 97

tally_lock = threading.Lock()          # オーブンのスレッドとループが一緒に書き換える帳簿
tally = {"plates": 0}

def count_plate():
    with tally_lock:                   # ロックの中にはawaitも長い仕事もない
        tally["plates"] += 1

def old_oven(item):                    # 古いオーブン:ブロッキング → スレッド
    time.sleep(0.4)
    count_plate()
    return f"{item} baked"

async def stove_dish(n, stoves):       # コンロ料理:セマフォ
    async with stoves:
        await asyncio.sleep(0.2)
    count_plate()
    return f"#{n}"

async def main():
    loop = asyncio.get_running_loop()
    stoves = asyncio.Semaphore(3)
    with ProcessPoolExecutor(max_workers=2) as dough_room:
        async with asyncio.timeout(5):         # コース全体の締め切り
            results = await asyncio.gather(
                loop.run_in_executor(dough_room, knead, 3_000_000),
                asyncio.to_thread(old_oven, "bread"),
                *(stove_dish(n, stoves) for n in range(1, 7)),
            )
    print(results)
    print(tally)

if __name__ == "__main__":
    asyncio.run(main())
# [56, 'bread baked', '#1', '#2', '#3', '#4', '#5', '#6']
# {'plates': 7}
仕事担当道具
ホール全体、待つことルーasyncio.run, gather
生地(計算)支店の厨房run_in_executor + ProcessPoolExecutor
古いオーブン(ブロッキング)オーブンの前の料理人asyncio.to_thread
コンロ料理コンロ3つasyncio.Semaphore(3)
帳簿(共有状態)黒板のロックthreading.Lock(スレッドとループが一緒に使うので)
締め切りシェフのタイマーasyncio.timeout(5)

帳簿はオーブンのスレッドとループの両方から書き換えるので、threading.Lock を使いました。ループ側でこのロックを取っても大丈夫なのは、ロックの中に await がなく、数字をひとつ書き換えてすぐ放すからです。11限目で見た事故は、ロックを握ったまま await で譲ったときに起きます。

12-4. 並行処理バグのチェックリスト (6分)

評論家の前で厨房が止まらないように、営業前にこの6つを確認します。

チェック症状処方限目
共有状態数がときどき合わない読み込み〜書き込みを Lock でくくるか、キューで持ち主をひとりに3, 11
ロックの順番ときどき永遠に止まる全員が同じ順番でロックする、acquire(timeout=)5
ブロッキング呼び出しasyncioがまるごと遅いto_thread、debug=True で探す11
握りつぶしたキャンセル制限時間とTaskGroupが効かないCancelledError は後片づけのあと raise し直す10
参照のないタスクバックグラウンドの仕事がときどき消えるタスクを集めてつかんでおくか、TaskGroup9
終わらないキューjoin() が戻ってこない取り出した分だけ task_done()、終了の合図(None、shutdown())6, 11

「参照のないタスク」の処方はこうです。タスクを集合に入れてつかんでおき、終わったら自分で抜けるようにします。

import asyncio

running = set()                        # タスクをつかんでおく場所

async def wipe(table):
    await asyncio.sleep(0.05)
    print("wiped", table)

async def main():
    for t in ("T1", "T2"):
        task = asyncio.create_task(wipe(t))
        running.add(task)
        task.add_done_callback(running.discard)   # 終わったら自分で抜ける
    await asyncio.sleep(0.1)
    print(len(running))

asyncio.run(main())
# wiped T1
# wiped T2
# 0

「終わらないキュー」は、task_done() をひとつでも忘れると起きます。怪しいときは、join() に制限時間をかけて確かめます。

import asyncio

async def cook(q):
    while True:
        await q.get()
        await asyncio.sleep(0.01)
        # q.task_done()  ← 忘れた

async def main():
    q = asyncio.Queue()
    for n in range(3):
        q.put_nowait(n)
    worker = asyncio.create_task(cook(q))
    try:
        async with asyncio.timeout(0.3):
            await q.join()
    except TimeoutError:
        print("join never finished:", q.qsize(), "in queue")
    worker.cancel()

asyncio.run(main())
# join never finished: 0 in queue

キューは空なのに(0 in queue)、join() は終わりません。取り出した伝票3枚が「終わった」という印をもらえていないからです。

12-5. 11夜の厨房をもう一度回す (4分)

これまでの夜の厨房を、ひとつの実験台に集めました。同じ8つの注文を、方式だけ変えて回してみてください。料理人とコンロのスライダーは、その値を使う方式でだけ効きます。

厨房タイムライン

注文

6

料理人

3

コンロ

2

調理

待ち

キャンセル

失敗

注文8件、それぞれ0.3秒。一人で順番に、料理人を増やす、コンロ制限、出張料理人、gatherでまとめて、を順に回して、棒の形とかかった時間を比べてみてください。
  • 一人で順番に:1行で約2.4秒(1限目)。
  • 料理人4人:4行が重なって約0.6秒(2限目)。コンロを2つに制限すると黄色い待ちが生まれ、約1.2秒(4限目)。
  • 出張料理人:後ろの番号ほど短い注文なので戻ってきた順が入り乱れ、約0.4秒(7限目)。
  • gatherでまとめて:8卓がすべて重なって約0.3秒(9限目)。料理人の数に関係なく、ルーひとりで。

閉店後 (10分)

評論家のコースを達成した設定はコンロ3、制限時間0.6秒、再試行1です。サーバーの実験台のエンジンで実際に回して確かめた値です。

コンロ制限時間再試行提供 · 失敗かかった時間目標
20.4秒00 · 12約2.4秒失敗
3オフ012 · 0約3.5〜3.6秒時間オーバー
4オフ012 · 0約3.0秒コンロ超過
30.4秒20 · 12約4.9秒失敗
30.6秒09 · 3約2.2秒3皿失敗
30.6秒112 · 0約2.7秒成功
# 12-2のservice関数で
asyncio.run(service(3, 0.6, 1))
# stoves=3 limit=0.6 retries=1: served 12/12, 2.7s
  • 0.6秒の制限は、ふつうの皿(0.5秒)は通し、最初に長引いた皿(1.5秒)だけを0.6秒で切ります。
  • 切られた3皿はコンロを手放し、列の最後でもう一度試して0.5秒で終わります。
  • 制限時間をもう少し縮めて0.55秒にしても目標は達成できます(約2.65秒)。0.7秒に延ばすと約2.9秒で3秒ぎりぎり、0.8秒なら3秒を超えます。

12皿目が出て2.7秒。シェフのタイマーが止まります。評論家は手帳に何行か書きつけ、麺の器を空にして立ち上がります。レジの前で、手帳を1枚破ってシェフに手渡します。

「12皿、一度も冷めずに出てきました。待ち時間はあったけれど、ぼんやり待っている人は誰もいなかった。また来ます。今度はただのお客として」

評論家が帰り、厨房の明かりがひとつずつ消えていきます。シェフがエプロンのポケットから最後のレシピカードを取り出し、副料理長に手渡します。「12の夜だったね。明日の夜10時も店は開く。今度はあんたが注文書を書く番。1行目に何を書く?」

12の夜をふりかえって

限目その夜残った1行
1初日の行列:並行と並列待つ仕事と手がふさがる仕事を分ける。並行はかわるがわる、並列は同じ瞬間に。
2料理人を増やす:threadingの基本Thread(target=)、start()、join()。結果は自分で持ち帰る。
3黒板の数が合わない:競合状態とLock読み込み〜書き込みのあいだに割り込まれると数が狂う。with lock: は短く。
4コンロは3つだけ:同期の道具Semaphore、Event、Condition、Barrier。
5包丁とまな板:デッドロック同じ順番でロックするか、取れなければ置く。
6伝票差し:queue生産者と消費者をキューで切り離し、task_done() と join() で終わりを知る。
7出張料理人:concurrent.futuresThreadPoolExecutor、Future、as_completed と map。
8生地はなぜ速くならない:GILと並列計算はプロセスで、または3.14tで。
9一人で回すホール係:asyncioの第一歩async def、await、gather、create_task。
10団体予約のキャンセル:構造化された並行処理TaskGroup、except*、asyncio.timeout、キャンセルは送出し直す。
11止まったホール:ブロッキングと組み合わせブロッキングは to_thread へ、asyncio版の道具はasyncioの中だけで。

営業日誌 — 十二日目の夜。評論家の12皿を、コンロ3、制限時間0.6秒、再試行1で約2.7秒ですべて出した。ホールはasyncio、生地はプロセス、古いオーブンはスレッド、コンロはセマフォ。明日から、注文書は副料理長が書く。

練習問題

  1. 実験台で目標を達成したあと、注文を8件に減らすと、コンロ2でも3秒以内に終えられるかどうか、制限時間と再試行を変えながら探してみてください。
  2. 12-2の処理係に、再試行のあいだの休み(await asyncio.sleep(0.05 * 2 ** attempt))を入れてみてください。コンロを確保する前に休むべきでしょうか、確保したあとに休むべきでしょうか? 両方回して、かかった時間を比べてみてください。
  3. 自分が作りたいプログラムをひとつ選び、12-1の表と12-4のチェックリストで設計してみてください。どの道具をなぜ選ぶのか、1行ずつ書けば十分です。

シェフのレシピカード

  • まず問う:待つ仕事か(asyncio、スレッド)、手がふさがる仕事か(プロセス、3.14t)?

  • asyncioの中のブロッキングはto_thread、重い計算はrun_in_executor(プロセスプール)。

  • 資源の上限はSemaphore、時間の上限はasyncio.timeout、一緒に終える仕事はTaskGroup。

  • 再試行はコンロを手放して列の最後から。制限時間は「ふつうより長く、しくじったものより短く」。

  • チェックリスト:共有状態、ロックの順番、ブロッキング呼び出し、握りつぶしたキャンセル、参照のないタスク、終わらないキュー。

  • 厨房はプログラムだ。棒を描いてみれば見えてくる。

← 前へ

11限目 · 止まったホール:ブロッキングと組み合わせ

新しい授業・講座のお知らせを受け取る

この講座に授業が追加されたり、続きの講座が公開されたりしたらメールでお知らせします。

収集項目:メールアドレス・目的:この講座の新しい授業・公開のお知らせ・保有期間:解除のご依頼まで。他の目的には使わず、第三者にも提供しません。お知らせメールに返信いただければすぐに削除します。

質問と回答

この回について気になることを書き込んでください。すべての言語のページの質問がここに集まります。

リンクや書式なしで、そのままの文字で表示されます。

R

ReflexKR

触って学ぶPythonとReflex。すべての例はReflex 0.10.0で実際に動かして確認しています。

ReflexKRはバナー広告なし・無料で運営しています。役に立ったら、コーヒー一杯で応援してください。 ☕ コーヒーで応援する

アフィリエイトと支援について
한국어English日本語简体中文EspañolDeutsch

© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0

Built with Reflex