R

ReflexKR

Reflex公式ドキュメント

深夜厨房の並行処理

·

講座の概要

1

初日の行列:並行と並列

2

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

3

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

4

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

5

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

6

伝票差し:queue

7

出張料理人:concurrent.futures

8

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

9

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

10

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

11

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

12

評論家の夜:総まとめ

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

時間 4:10–5:00

伝票差し:queue

今夜の注文書

#06 · 六日目の夜

伝票差し

ピンチ

ホールからは注文が押し寄せるのに、ミンホとスアは同じ伝票を同時につかみ、タクさんは手持ちぶさた。誰が何を作るのか、誰も決めていなかった。

目標

12の注文を料理人3人で分け、漏れなく一度ずつだけ作る。そして、すべて終わったことを確実に知る。

深夜食堂 ドンシ

六日目の夜、金曜日。ホール係が伝票を持って厨房に駆け込み、手近な誰かに渡していきます。ミンホとスアが同じ伝票を同時に手に取り、同じ麺が2杯できあがります。そのあいだタクさんは、包丁を持ったまま仕事が来るのを待っています。

ユン・ソヨンシェフが、厨房の入り口の上に長い鉄の棒を打ちつけます。伝票差しです。「ホールはここに差すだけ。料理人はここから抜くだけ。お互いに話しかけないこと」

6-1. 差す人と抜く人 (8分)

注文を生み出す側(ホール係)と処理する側(料理人)を切り離し、そのあいだに列をひとつ置く設計を生産者・消費者パターンと呼びます。Pythonでは queue.Queue がその列です。

import queue

rail = queue.Queue()        # 伝票差し
for n in range(1, 5):
    rail.put(f"#{n}")       # 差す
print(rail.qsize(), rail.get(), rail.get(), rail.qsize())
# 4 #1 #2 2

先に差した伝票が先に出てきます(FIFO、先入れ先出し)。では、ホール係1人と料理人2人をスレッドで動かしてみましょう。

import threading
import time

rail = queue.Queue()
served = []

def hall():                          # 生産者:注文を受けて差す
    for n in range(1, 7):
        time.sleep(0.05)
        rail.put(f"#{n}")

def cook(name):                      # 消費者:伝票差しから抜いて作る
    for _ in range(3):
        order = rail.get()           # 伝票差しが空ならここで待つ
        time.sleep(0.1)
        served.append((name, order))

ts = [threading.Thread(target=hall),
      threading.Thread(target=cook, args=("minho",)),
      threading.Thread(target=cook, args=("sua",))]
for t in ts: t.start()
for t in ts: t.join()
print(sorted(order for _, order in served))
# ['#1', '#2', '#3', '#4', '#5', '#6']
  • 6つの注文が一度ずつだけ作られました。二人の料理人が同じ伝票をつかむことはありません。
  • コードのどこにも Lock がありません。Queue の中にはすでにロックと4限目の Condition が入っていて、put と get が勝手に一度に一人ずつさばいてくれます。キューがロックの代わりをするのです。
  • get() は伝票差しが空なら、伝票が差されるまで待ちます。料理人が「注文ある?」と聞きながら忙しく回り続ける必要はありません。

スレッド同士でデータをやり取りする必要があるなら、共有変数にロックをかけるより、キューで受け渡すほうを先に考えてください。ロックを直接扱うコードが減るほど、3限目と5限目のような事故も減ります。

6-2. すべての注文が終わったと知る方法:task_doneとjoin (8分)

さっきの例では、料理人ごとに「3つ作ったら帰る」と決めていました。実際の厨房では、注文がいくつ来るかわかりません。料理人はずっと伝票差しを見張っていて、シェフは差した注文がすべて終わったかだけを知りたいのです。

rail = queue.Queue()

def cook(name):
    while True:
        order = rail.get()
        time.sleep(0.1)
        rail.task_done()          # 「この伝票は終わった」

for name in ("minho", "sua", "tak"):
    threading.Thread(target=cook, args=(name,), daemon=True).start()

start = time.perf_counter()
for n in range(1, 10):
    rail.put(f"#{n}")
rail.join()                       # 差した伝票がすべて終わるまで
print(f"all 9 done: {time.perf_counter() - start:.2f}s")
# all 9 done: 0.30s

キューは内部でまだ終わっていない注文の数を数えています。

操作終わっていない注文の数
put() 1回1増える
task_done() 1回1減る
join()0になるまで待つ
  • get() で取り出しただけでは終わったことになりません。料理が終わったあとに task_done() を呼んで、はじめて数が減ります。
  • 料理人のスレッドは while True で永遠に伝票差しを見張っているので、daemon=True にしました。注文がすべて終われば join() が戻り、プログラムが終わるときに料理人たちも一緒に帰ります。
  • task_done() を取り出した回数より多く呼ぶと、ValueError: task_done() called too many times が出ます。

シェフのひとこと

task_doneを呼び忘れたら、joinは永遠に戻らない。料理中に例外が出ても同じ。だから私はいつもtry/finallyで包む。「finally: rail.task_done()」。皿を割っても、伝票は下ろさなきゃね。

6-3. 伝票差しがいっぱいになったら (10分)

伝票差しには枠の数を決められます。Queue(maxsize=2) なら伝票は2枚までしか差せません。いっぱいの伝票差しに put() すると、入れる側が待ちます。厨房が詰まったら、ホールが注文を受けるペースを落とすわけです。

rail = queue.Queue(maxsize=2)     # 伝票は2枚までしか差せない
rail.put("#1")
rail.put("#2")
print(rail.full())
# True

try:
    rail.put("#3", timeout=0.5)   # 0.5秒待っても空きが出なければ
except queue.Full:
    print("Full after 0.5s")
# Full after 0.5s

try:
    rail.put_nowait("#3")         # = put("#3", block=False)
except queue.Full:
    print("Full right away")
# Full right away

print(rail.get_nowait(), rail.get(timeout=0.5))
# #1 #2

try:
    rail.get(timeout=0.5)         # 伝票差しが空になった
except queue.Empty:
    print("Empty after 0.5s")
# Empty after 0.5s
呼び方伝票差しがいっぱい/空のとき
put(x), get()空きが出るまで(伝票が来るまで)待つ
put(x, timeout=0.5), get(timeout=0.5)0.5秒まで待ってから queue.Full / queue.Empty
put_nowait(x), get_nowait()待たずにすぐ queue.Full / queue.Empty

maxsize を指定しないか0にすると、伝票差しはどこまでも長くなります。ホールが厨房よりずっと速ければ、伝票が際限なくたまってメモリを埋めていきます。大きさを決めておけば、速い側が遅い側に合わせて待ちます。これを背圧(backpressure、バックプレッシャー)と呼びます。

rail = queue.Queue(maxsize=2)

def cook():                           # 遅い料理人1人
    while True:
        rail.get()
        time.sleep(0.1)
        rail.task_done()

threading.Thread(target=cook, daemon=True).start()

start = time.perf_counter()
for n in range(1, 7):
    rail.put(f"#{n}")                 # 速いホール:空きがなければputで待つ
print(f"hall done at {time.perf_counter() - start:.1f}s")
rail.join()
print(f"kitchen done at {time.perf_counter() - start:.1f}s")
# hall done at 0.3s
# kitchen done at 0.6s

ホールは伝票6枚を一瞬で差したかったのですが、伝票差しが2枠なので0.3秒間 put で待ちました。maxsize を外すとホールはすぐに終わり、伝票差しに伝票がたまります。

下のタイムラインで実際に確かめてみてください。上の段がホール係、下の段が料理人です。ホール係は注文を受けるのに0.1秒(オレンジ色)、伝票差しに空きが出るまで待つ時間(黄色)を使います。

厨房タイムライン

注文

6

ホール係

1

料理人

3

伝票差しの大きさ

3

調理

待ち

キャンセル

失敗

ホール係2人、料理人2人、伝票差し3枠、注文10件。伝票差しを1に減らすとホールの段の黄色い待ちが長くなり、8に増やすとホールは0.5秒ほどで終わります。料理人を増やすと全体の時間が縮みます。
  • 初期値(料理人2人、伝票差し3枠)で回すと約1.6秒かかり、伝票差しには伝票が最大3枚たまります。ホール係は0.3秒あたりから待ち始めます。
  • 伝票差しを8枠に増やすと、ホール係は待たずに約0.5秒で仕事を終えます。それでも全体の時間は約1.6秒のままです。厨房が遅いことは、伝票差しを増やしても解決しません。
  • 料理人を4人に増やすと、全体の時間は約1.0秒に縮みます。ボトルネックがどこにあるかは、黄色い棒がどの段に長く出るかを見ればわかります。

6-4. 営業終了の合図:番兵とshutdown (9分)

daemon=True で料理人をほったらかしにする代わりに、「今日の営業は終わり」を知らせて、きちんと帰らせたいこともあります。昔からある方法は番兵(sentinel)、つまり「営業終了」と書かれた特別な伝票を差すことです。

STOP = None                         # 番兵:「営業終了」の伝票
rail = queue.Queue()
served = []

def cook(name):
    while True:
        order = rail.get()
        if order is STOP:
            break                   # 番兵を受け取ったら帰る
        time.sleep(0.05)
        served.append(order)

cooks = [threading.Thread(target=cook, args=(n,)) for n in ("minho", "sua")]
for t in cooks: t.start()
for n in range(1, 7):
    rail.put(f"#{n}")
for _ in cooks:
    rail.put(STOP)                  # 料理人の数だけ差す
for t in cooks: t.join()
print(len(served), [t.is_alive() for t in cooks])
# 6 [False, False]

番兵は料理人の人数分差さなければなりません。1枚だけだと一人しか帰れず、残りは空の伝票差しの前で永遠に待ち続けます。伝票差しはFIFOなので、番兵は先に差された注文がすべて抜かれたあとでようやく出てきます。

Python 3.13からは、伝票差しそのものを閉じられます。Queue.shutdown() を呼ぶと、そのあとの put() は queue.ShutDown を送出します。get() は残った伝票を全部渡し終えてから queue.ShutDown を送出します。料理人の人数を数える必要はありません。

rail = queue.Queue()
served = []

def cook(name):
    while True:
        try:
            order = rail.get()
        except queue.ShutDown:      # 伝票差しが閉じられ、残りの伝票もない
            return
        time.sleep(0.05)
        served.append(order)
        rail.task_done()

cooks = [threading.Thread(target=cook, args=(n,)) for n in ("minho", "sua")]
for t in cooks: t.start()
for n in range(1, 7):
    rail.put(f"#{n}")
rail.shutdown()                     # もう受け付けない。差してある伝票は最後まで作る
for t in cooks: t.join()
print(len(served))
# 6

try:
    rail.put("#7")
except queue.ShutDown:
    print("rail is closed")
# rail is closed

急いで店を閉めなければならないときは shutdown(immediate=True) を使います。伝票差しに残った伝票をすべて捨て、待っていた get() もただちに queue.ShutDown を送出します。

rail = queue.Queue()
served = []

def cook(name):
    while True:
        try:
            order = rail.get()
        except queue.ShutDown:
            return
        time.sleep(0.1)
        served.append(order)
        rail.task_done()

cooks = [threading.Thread(target=cook, args=(n,)) for n in ("minho", "sua")]
for t in cooks: t.start()
for n in range(1, 7):
    rail.put(f"#{n}")
time.sleep(0.15)                    # 二人が#3、#4を作っている最中
rail.shutdown(immediate=True)       # 伝票差しに残った#5、#6は捨てて今すぐ閉じる
rail.join()                         # 手に持った#3、#4が終われば戻る
for t in cooks: t.join()
print(sorted(served), rail.qsize())
# ['#1', '#2', '#3', '#4'] 0
shutdown()shutdown(immediate=True)
新しい put()queue.ShutDownqueue.ShutDown
差してあった伝票最後まで渡す捨てる(終わっていない注文の数からも引く)
get()伝票差しが空になってから queue.ShutDownすぐに queue.ShutDown
料理人が手に持っている注文最後まで作る最後まで作る

immediate=True でも、すでに手に持っている注文まで取り上げはしません。だから join() は、それらの注文の task_done() が呼ばれたあとに戻ります。

6-5. 形の違う伝票差し (5分)

queue モジュールには、取り出す順番が違う伝票差しがほかにもあります。

PriorityQueue:小さい値から取り出します。VIPテーブルの注文を先に作るときに使います。(優先度, 番号, 注文) のタプルで入れるのが定番です。

import itertools

vip_first = queue.PriorityQueue()
ticket = itertools.count(1)          # 優先度が同じなら先に来た順
for prio, dish in [(2, "noodle"), (1, "VIP soup"), (2, "rice"), (1, "VIP noodle")]:
    vip_first.put((prio, next(ticket), dish))

while not vip_first.empty():
    print(vip_first.get())
# (1, 2, 'VIP soup')
# (1, 4, 'VIP noodle')
# (2, 1, 'noodle')
# (2, 3, 'rice')

真ん中の番号がないと、優先度が同じときに3つ目の値同士を比べます。注文が辞書のように比較できない値だと、エラーで落ちます。

pq = queue.PriorityQueue()
pq.put((1, {"dish": "soup"}))
try:
    pq.put((1, {"dish": "rice"}))   # 優先度が同じだとdict同士を比べようとして
except TypeError as e:
    print(e)
# '<' not supported between instances of 'dict' and 'dict'

皿の山のように最後のものから取り出す LifoQueue、いちばんシンプルな SimpleQueue もあります。

plates = queue.LifoQueue()           # 皿の山:最後に載せたものから
for p in ("p1", "p2", "p3"):
    plates.put(p)
print(plates.get(), plates.get(), plates.get())
# p3 p2 p1

simple = queue.SimpleQueue()         # いちばんシンプルな伝票差し:maxsize、task_done、joinがない
simple.put("#1")
print(simple.get(), hasattr(simple, "task_done"))
# #1 False
伝票差し取り出す順番使いどころ
Queue先に入れたものからほとんどすべての場面
PriorityQueue小さい値からVIPを先に、急ぎの仕事を先に
LifoQueue最後に入れたものから皿の山、新しいものから処理
SimpleQueue先に入れたものから大きさの制限や完了の追跡が要らないとき

閉店後 (10分)

今夜の目標は、12の注文を料理人3人で漏れなく一度ずつだけ作り、すべて終わったことを確実に知ることでした。ホール係2人が交互に注文を受け、伝票差しは4枠です。

rail = queue.Queue(maxsize=4)        # 伝票差しは4枠
served = []

def hall(orders):                    # ホール係:注文を受けて差す
    for n in orders:
        time.sleep(0.02)
        rail.put(f"#{n}")

def cook(name):
    while True:
        try:
            order = rail.get()
        except queue.ShutDown:
            return
        time.sleep(0.1)
        served.append((name, order))
        rail.task_done()

start = time.perf_counter()
halls = [threading.Thread(target=hall, args=(range(k, 13, 2),)) for k in (1, 2)]
cooks = [threading.Thread(target=cook, args=(n,)) for n in ("minho", "sua", "tak")]
for t in halls + cooks: t.start()
for t in halls: t.join()             # 注文の受け付けが終わったら
rail.shutdown()                      # 伝票差しを閉じて
rail.join()                          # 差した伝票がすべて終わるまで
for t in cooks: t.join()
orders = [o for _, o in served]
print(len(orders), len(set(orders)), f"{time.perf_counter() - start:.2f}s")
# 12 12 0.44s  (時間はおおよそ)

作った料理は12、異なる注文も12。同じ注文が二度出ることも、抜けることもありませんでした。タクさんもきっちり自分の分をこなしました。このコードには Lock が1行もありません。

シェフが伝票差しを軽く叩きます。「これでホールと厨房はお互いに話さなくていい。でも来週の金曜に団体の注文が30件入ったら? 料理人を30人雇う?」

営業日誌 — 六日目の夜。伝票差し(Queue)を付けたら、同じ注文を二度作ることがなくなった。伝票差しを閉じ(shutdown)、joinですべての注文が終わったのを確かめてから閉店。

練習問題

  1. 6-2の cook を、#5 の注文だけ ValueError が出るように変えてみてください。rail.join() は戻るでしょうか? 永遠に待たないように waiter = threading.Thread(target=rail.join, daemon=True) で起動し、waiter.join(timeout=1) のあと waiter.is_alive() を表示して確かめてから、try/finally で直してみてください。
  2. タイムラインでホール係4人、料理人1人、伝票差し1枠にして回してみてください。黄色い棒がどの段に出るか、ボトルネックがどこかを説明してみてください。
  3. 閉店後のコードを PriorityQueue に変えて、偶数番の注文をVIPとして先に作るようにしてみてください。shutdown() は PriorityQueue でも同じように使えます。

シェフのレシピカード

  • 生産者・消費者:差す側は rail.put(x)、抜く側は rail.get()。キューがロックの代わりをする。

  • 全部終わった? 仕事が終わるたびに rail.task_done()、待つ側は rail.join()。

  • Queue(maxsize=n):いっぱいになると入れる側が待つ(背圧)。timeout=、put_nowait/get_nowait、queue.Full/queue.Empty。

  • 終了:番兵 None を料理人の数だけ、または3.13からは rail.shutdown() → queue.ShutDown。immediate=True なら残りの伝票を捨てる。

  • PriorityQueue((優先度, 番号, 注文))、LifoQueue(皿の山)、SimpleQueue(いちばんシンプル)。

← 前へ

5限目 · 包丁とまな板:デッドロック

次へ →

7限目 · 出張料理人:concurrent.futures

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

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

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

質問と回答

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

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

R

ReflexKR

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

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

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

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

Built with Reflex