今夜の注文書
#06 · 六日目の夜
ピンチ
ホールからは注文が押し寄せるのに、ミンホとスアは同じ伝票を同時につかみ、タクさんは手持ちぶさた。誰が何を作るのか、誰も決めていなかった。
目標
12の注文を料理人3人で分け、漏れなく一度ずつだけ作る。そして、すべて終わったことを確実に知る。
深夜食堂 ドンシ
六日目の夜、金曜日。ホール係が伝票を持って厨房に駆け込み、手近な誰かに渡していきます。ミンホとスアが同じ伝票を同時に手に取り、同じ麺が2杯できあがります。そのあいだタクさんは、包丁を持ったまま仕事が来るのを待っています。
ユン・ソヨンシェフが、厨房の入り口の上に長い鉄の棒を打ちつけます。伝票差しです。「ホールはここに差すだけ。料理人はここから抜くだけ。お互いに話しかけないこと」
注文を生み出す側(ホール係)と処理する側(料理人)を切り離し、そのあいだに列をひとつ置く設計を生産者・消費者パターンと呼びます。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']Lock がありません。Queue の中にはすでにロックと4限目の Condition が入っていて、put と get が勝手に一度に一人ずつさばいてくれます。キューがロックの代わりをするのです。get() は伝票差しが空なら、伝票が差されるまで待ちます。料理人が「注文ある?」と聞きながら忙しく回り続ける必要はありません。スレッド同士でデータをやり取りする必要があるなら、共有変数にロックをかけるより、キューで受け渡すほうを先に考えてください。ロックを直接扱うコードが減るほど、3限目と5限目のような事故も減ります。
さっきの例では、料理人ごとに「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()」。皿を割っても、伝票は下ろさなきゃね。
伝票差しには枠の数を決められます。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
調理
待ち
キャンセル
失敗
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'] 0shutdown() | shutdown(immediate=True) | |
|---|---|---|
新しい put() | queue.ShutDown | queue.ShutDown |
| 差してあった伝票 | 最後まで渡す | 捨てる(終わっていない注文の数からも引く) |
get() | 伝票差しが空になってから queue.ShutDown | すぐに queue.ShutDown |
| 料理人が手に持っている注文 | 最後まで作る | 最後まで作る |
immediate=True でも、すでに手に持っている注文まで取り上げはしません。だから join() は、それらの注文の task_done() が呼ばれたあとに戻ります。
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 | 先に入れたものから | 大きさの制限や完了の追跡が要らないとき |
今夜の目標は、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ですべての注文が終わったのを確かめてから閉店。
cook を、#5 の注文だけ ValueError が出るように変えてみてください。rail.join() は戻るでしょうか? 永遠に待たないように waiter = threading.Thread(target=rail.join, daemon=True) で起動し、waiter.join(timeout=1) のあと waiter.is_alive() を表示して確かめてから、try/finally で直してみてください。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(いちばんシンプル)。
新しい授業・講座のお知らせを受け取る
この講座に授業が追加されたり、続きの講座が公開されたりしたらメールでお知らせします。
R
ReflexKR
触って学ぶPythonとReflex。すべての例はReflex 0.10.0で実際に動かして確認しています。
ReflexKRはバナー広告なし・無料で運営しています。役に立ったら、コーヒー一杯で応援してください。 ☕ コーヒーで応援する
アフィリエイトと支援について© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0
質問と回答
この回について気になることを書き込んでください。すべての言語のページの質問がここに集まります。