今夜の注文書
#02 · 二日目の夜
ピンチ
ミンホ、スア、タクさんが初出勤。3人ともよく動いているのに、誰がいつ何を仕上げたのか誰にもわからない。
目標
6つの注文を3人で分けて0.6秒以内に終わらせ、誰が何秒に何を仕上げたかを記録に残す。
深夜食堂 ドンシ
二日目の夜、9時半。ラインコックの3人がエプロンを締めます。ミンホはもう包丁を握り、スアは注文書を順番に並べ、タクさんはゆっくりとコンロを拭いています。
10時になると注文が入り、3人はそれぞれ鍋を火にかけます。ところが30分後、シェフが「3番テーブルのうどん、誰が作った?」と聞くと、誰も答えられません。
シェフが首のタイマーを指でとんと叩きます。「料理人を雇うところまではできた。今日は名札をつけて、仕事を任せて、終わるまで待つやり方を覚えるよ」
Pythonでは、料理人一人がスレッド(thread)ひとつです。同じ厨房(メモリ)を共有しながら、それぞれが自分の仕事を別々に進めます。スレッドは threading.Thread で作ります。
import threading
import time
def cook(name, dish):
print(f"{name}: start {dish}")
time.sleep(0.3) # スープが煮えるのを待つ
print(f"{name}: done {dish}")
minho = threading.Thread(target=cook, args=("minho", "ramen"))
minho.start() # ミンホが仕事を始める
print("chef: next order") # シェフは待たずに次の仕事へ
minho.join() # ミンホが終わるまで待つ
print("chef: served")
# minho: start ramen
# chef: next order
# minho: done ramen
# chef: servedtarget= にはスレッドがする仕事(関数)を、args= にはその関数に渡す引数をタプルで渡します。関数を呼ぶのではなく関数そのものを渡すので、cook() ではなく cook です。Thread(...) を作っただけでは何も起きません。料理人を雇っただけの状態です。start() を呼んではじめて、新しいスレッドで cook が動き出します。start() はすぐに戻ってきます。だからミンホがラーメンを作っているあいだに、シェフ(プログラムを最初に始めたメインスレッド)は次の行へ進みます。join() はそのスレッドが終わるまで待ちます。「ミンホが皿を差し出すまで、パスの前で待つ」ということです。3人にひとつずつ任せると、こうなります。
def cook(name, dish):
time.sleep(0.3)
print(f"{name}: done {dish}")
crew = [("minho", "ramen"), ("sua", "udon"), ("tak", "soba")]
threads = [threading.Thread(target=cook, args=c) for c in crew]
start = time.perf_counter()
for th in threads:
th.start() # まず3人全員を送り出して
for th in threads:
th.join() # それから全員を待つ
print(f"{time.perf_counter() - start:.2f}s")
# 例:
# sua: done udon
# minho: done ramen
# tak: done soba
# 0.30s3人が終わる順番は、実行するたびに変わります。3人とも0.3秒後にほぼ同時に終わるので、OSが誰を先に起こすか次第だからです。並行処理のコードでは「出力の順番」は約束ではありません。
start() と join() を同じループの中に書いてはいけません。th.start(); th.join() を交互にやると、一人が終わらないと次の人が出発できないので、結局0.9秒かかります。
待つ時間に上限をつけることもできます。join(timeout=秒) はその時間だけ待って戻り、まだ仕事中かどうかは is_alive() で確かめます。
slow = threading.Thread(target=time.sleep, args=(1.0,))
slow.start()
slow.join(timeout=0.2)
print(slow.is_alive()) # True:まだ煮えている途中
slow.join()
print(slow.is_alive()) # False厨房タイムライン
注文
6
料理人
3
調理
待ち
キャンセル
失敗
Thread を作って start()、join() を呼んでいます。Thread には run() というメソッドもあります。名前だけ見ると「実行」ですが、直接呼ぶと落とし穴にはまります。
def cook(name, dish):
time.sleep(0.3)
print(f"{threading.current_thread().name}: done {dish}")
threads = [threading.Thread(target=cook, args=c) for c in crew]
start = time.perf_counter()
for th in threads:
th.run() # start()ではなくrun()
print(f"{time.perf_counter() - start:.2f}s")
# MainThread: done ramen
# MainThread: done udon
# MainThread: done soba
# 0.90srun() は target を呼ぶだけの普通のメソッドです。新しいスレッドは作らず、今のスレッドで順番に実行します。だから3回とも MainThread がこなし、時間も 0.3 × 3 = 0.9秒です。start() です。start() が新しいスレッドを作り、そのスレッドの中で run() を呼びます。スレッドは一度しか出発できません。同じスレッドに start() を2回呼ぶとエラーになります。同じ仕事をもう一度したいなら、Thread を新しく作ってください。
th = threading.Thread(target=time.sleep, args=(0.1,))
th.start()
th.start()
# RuntimeError: threads can only be started onceシェフのひとこと
args=("ramen")にはカンマがないから、タプルじゃなくてただの文字列。すると r、a、m、e、n の5文字がばらばらに渡って、'cook() takes 1 positional argument but 5 were given' が出る。引数がひとつなら、必ず args=("ramen",) みたいにカンマを打って。それと、スレッドの中で起きた例外はシェフのところまで上がってこない。画面に 'Exception in thread ...' とトレースバックが出るだけで、プログラムはそのまま続いちゃう。見逃さないように、ログは必ず見ること。
今夜の問題は「誰がやったかわからない」ことでした。スレッドには名札をつけられます。
def cook(dish):
me = threading.current_thread() # このコードを実行中のスレッド
print(me.name, dish, threading.get_native_id())
time.sleep(0.2)
a = threading.Thread(target=cook, args=("ramen",), name="minho")
b = threading.Thread(target=cook, args=("udon",)) # 名前を渡さないと?
a.start(); b.start()
print([t.name for t in threading.enumerate()])
print(threading.active_count())
a.join(); b.join()
print(threading.current_thread() is threading.main_thread())
print([t.name for t in threading.enumerate()])
# 例:
# minho ramen 25680
# Thread-1 (cook) udon 38072
# ['MainThread', 'minho', 'Thread-1 (cook)']
# 3
# True
# ['MainThread']| 道具 | 返すもの |
|---|---|
Thread(name="minho") | スレッドに名前をつける。渡さなければ Thread-1 (cook) のように番号と関数名がつく |
threading.current_thread() | いまこの行を実行しているスレッドのオブジェクト |
threading.main_thread() | プログラムを始めたメインスレッド(名前は MainThread)。今夜の厨房ではシェフ |
threading.enumerate() | いま生きているスレッドの一覧(メインスレッドを含む) |
threading.active_count() | その一覧の長さ |
threading.get_native_id() | OSがつけたスレッド番号。Linuxの top -H などのシステムツールに出る番号と同じ |
b が Thread-2 ではなく Thread-1 なのは、番号が名前のないスレッドにだけ振られるからです。get_native_id() の数字は実行するたびに変わります。Pythonの中で使う番号は th.ident(または threading.get_ident())で、どちらもスレッドが出発したあとでないと決まりません。enumerate() から消えます。退勤した料理人は名簿に載っていません。関数は return で結果を返しますが、スレッドはその値を受け取ってくれません。target 関数の戻り値はそのまま捨てられ、join() も何も返しません。
def make(dish):
time.sleep(0.1)
return f"{dish} ok"
th = threading.Thread(target=make, args=("ramen",))
th.start()
print(th.join()) # joinは何も返さない
# Noneそこで、結果を受け取る器をあらかじめ用意しておき、スレッドにそこへ盛ってもらいます。いちばん簡単なのは、料理人ごとに自分の枠を決めてあげる方法です。
dishes = ["ramen", "udon", "soba"]
results = [None] * len(dishes) # 枠を先に用意する
def cook(i, dish):
results[i] = make(dish) # 自分の枠にだけ書く
threads = [threading.Thread(target=cook, args=(i, d)) for i, d in enumerate(dishes)]
for th in threads:
th.start()
for th in threads:
th.join()
print(results)
# ['ramen ok', 'udon ok', 'soba ok']名前で引きたいなら dict を使います。
done = {}
def cook2(name, dish):
done[name] = make(dish)
ts = [threading.Thread(target=cook2, args=(n, d)) for n, d in [("minho", "ramen"), ("sua", "udon")]]
for t in ts:
t.start()
for t in ts:
t.join()
print(sorted(done.items()))
# [('minho', 'ramen ok'), ('sua', 'udon ok')]join() が終わってから読みます。その前に読むと、まだ None のままの枠があるかもしれません。ThreadPoolExecutor が返す Future がこの仕事を代わりにやってくれます。future.result() で皿を直接受け取れます。Pythonのプログラムは、メインスレッドが終わっても、daemonでないスレッドが全部終わるまで待ってから終了します。普通に作ったスレッドはどれもdaemonではありません。
def clean():
for i in range(1, 6):
print("cleaning", i)
time.sleep(0.1)
th = threading.Thread(target=clean) # daemon=False(デフォルト)
th.start()
time.sleep(0.25)
print("lights off")
# cleaning 1
# cleaning 2
# cleaning 3
# lights off
# cleaning 4
# cleaning 5シェフが「明かり消すよ」と声をかけても、掃除中の料理人が終わるまで店の扉は閉まりません。同じコードに daemon=True だけつけてみます。
th = threading.Thread(target=clean, daemon=True)
th.start()
time.sleep(0.25)
print("lights off", th.daemon)
# cleaning 1
# cleaning 2
# cleaning 3
# lights off Truejoin() を呼べばいいのです。join() はdaemonかどうかに関係なく待ちます。start() の前に決めます。指定しなければ作ったスレッドの値を引き継ぎますが、メインスレッドはdaemonではないので、普通は False です。Threadを継承してrunを書き換える。料理人に自分の状態(担当の料理、結果)を持たせておきたいときに使います。2-2で見たとおり start() が新しいスレッドで run() を呼ぶので、run() だけ書き直せば十分です。
class Cook(threading.Thread):
def __init__(self, name, dish):
super().__init__(name=name) # 必ず親の__init__から
self.dish = dish
self.result = None
def run(self): # start()が新しいスレッドでこれを呼ぶ
time.sleep(0.1)
self.result = f"{self.dish} ok"
tak = Cook("tak", "soba")
tak.start()
tak.join()
print(tak.name, tak.result)
# tak soba okthreading.Timer:n秒後に一度だけ。麺ゆでタイマーのように、決めた時間が過ぎたら関数を一度呼ぶスレッドです。鳴る前なら cancel() で止められます。
start = time.perf_counter()
def ring(msg):
print(f"{time.perf_counter() - start:.1f}s", msg)
noodles = threading.Timer(0.5, ring, args=("noodles up",))
noodles.start() # 0.5秒後に一度鳴る
eggs = threading.Timer(1.0, ring, args=("eggs up",))
eggs.start()
eggs.cancel() # 鳴る前に止めれば何も起きない
noodles.join()
eggs.join()
print("end")
# 0.5s noodles up
# endthreading.local():料理人ごとに別々のエプロンのポケット。同じ名前の変数なのに、スレッドごとに違う値が入ります。誰が入れたかによってポケットが違うのです。
pocket = threading.local()
def cook(name, n):
pocket.name = name
pocket.count = 0
for _ in range(n):
pocket.count += 1
time.sleep(0.01)
print(pocket.name, pocket.count)
ts = [threading.Thread(target=cook, args=a) for a in [("minho", 3), ("sua", 5)]]
for t in ts:
t.start()
for t in ts:
t.join()
print(hasattr(pocket, "name")) # シェフ(メインスレッド)のポケットは空
# minho 3
# sua 5
# Falsepocket はグローバル変数ひとつなのに、ミンホの count とスアの count は混ざりません。スレッドごとにデータベース接続をひとつずつ持たせるときによく使う方法です。
シェフのひとこと
スレッドを作るときは、まず名前をつけて。明け方にログを開いたら Thread-7、Thread-12 ばっかり並んでたら、誰がやらかしたのか絶対わからない。logging のフォーマットに %(threadName)s をひとつ入れておくだけで、行ごとに名前が出るから。
今夜の目標は、6つの注文を3人で分けて0.6秒以内に終わらせ、誰が何秒に何を仕上げたかを残すことでした。名札(name=)をつけ、仕上げるたびに共用の日誌に1行ずつ書いてもらいます。
start = time.perf_counter()
log = [] # (仕上がった時刻, 料理人, 注文)
def cook(dishes):
me = threading.current_thread().name
for dish in dishes:
time.sleep(0.3)
log.append((round(time.perf_counter() - start, 1), me, dish))
plan = {"minho": ["#1", "#4"], "sua": ["#2", "#5"], "tak": ["#3", "#6"]}
threads = [threading.Thread(target=cook, args=(d,), name=n) for n, d in plan.items()]
for th in threads:
th.start()
for th in threads:
th.join()
for row in sorted(log):
print(*row)
print(f"total {time.perf_counter() - start:.2f}s, {len(log)} orders")
# 0.3 minho #1
# 0.3 sua #2
# 0.3 tak #3
# 0.6 minho #4
# 0.6 sua #5
# 0.6 tak #6
# total 0.60s, 6 ordersargs=(d,) にカンマがあります。log.append(...) は一度で終わるリスト操作なので、複数のスレッドが一緒に呼んでも行が消えることはありません。日誌は join() のあとで並べ替えて読みます。シェフは日誌にざっと目を通してうなずきます。「よし、これで誰が何をしたかはわかる。じゃあ明日はもうひとつだけ試そう。4人がそれぞれ作った数を黒板のひとつの数字に足していったら、その数字は合ってると思う?」
営業日誌 — 二日目の夜。ミンホ、スア、タクさんが初出勤。名札をつけ、注文6件を2件ずつ分けて0.6秒で完了。誰が何秒に何を出したかを日誌に残した。
start() と join() を同じループの中に一緒に書くように変え、何秒になるか予想してから測ってみましょう。sleep の時間を変える)。全体の時間を決めるのは誰でしょう? タイムラインで1列だけ長くなる様子と比べてみてください。threading.Timer で「0.3秒ごとに1回、計3回」鳴る厨房タイマーを作ってみましょう。Timerは一度しか鳴らないので、鳴ったときに次のTimerを新しくスタートさせる必要があります。シェフのレシピカード
th = threading.Thread(target=関数, args=(引数,), name="minho") → th.start() → th.join()
run()を直接呼ぶと、新しいスレッドなしに今のスレッドで順番に実行。スレッドはstart()で、一度だけ。
誰がやっているか:current_thread().name、main_thread()、enumerate()、get_native_id()
スレッドは戻り値を返さない。先に用意したリストの枠やdictのキーに入れ、join()のあとで読む。
daemon=Trueはプログラムの終了時に待ってもらえない。最後までやるべき仕事には使わない。
Threadの継承(runのオーバーライド)、Timer(n秒後に一度、cancel)、local()(スレッドごとに別々のポケット)
新しい授業・講座のお知らせを受け取る
この講座に授業が追加されたり、続きの講座が公開されたりしたらメールでお知らせします。
R
ReflexKR
触って学ぶPythonとReflex。すべての例はReflex 0.10.0で実際に動かして確認しています。
ReflexKRはバナー広告なし・無料で運営しています。役に立ったら、コーヒー一杯で応援してください。 ☕ コーヒーで応援する
アフィリエイトと支援について© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0
質問と回答
この回について気になることを書き込んでください。すべての言語のページの質問がここに集まります。