こんにちは、おうどんです🍜
新人さん「おうどんさん、この処理、スレッドを増やせば速くなりますか? それともプロセスを増やすんですか?」
おうどん「いい質問。ちなみに、プロセスとスレッドの違いは?」
新人さん「……スレッドのほうが、細い?」
おうどん「麺の話じゃないんだよね」
(会話は説明用の架空の場面です)
細いのは、ちょっとだけ合っています。Python のドキュメントにも、スレッドは「プロセスの中の、より小さな単位」と書かれているので。
でも、太さで覚えると、あとで必ず困ります。
プロセスとスレッドは、並列処理、Oracle の PROCESSES、CPU の「4スレッド」など、いろんな記事の前提になっている言葉です。
今日は基礎講座として、この2つの違いを、うどん屋さんの厨房にたとえながら、Python と PowerShell で実際に確かめていきます。
最初に、初心者向けのチェックリストです。
- プロセスは「お店」、スレッドは「お店で働く店員」。1つのプロセスの中に、スレッドが1本以上いる
- 同じプロセスのスレッドは、メモリ(変数)を共有する。別のプロセスとは、ふつう共有しない
- 今動いているプロセスの番号(PID)は、PowerShell なら
Get-Process、Python ならos.getpid()で見られる
実践編の「Pythonのログ集計を並列化しても速くならない原因と対処」と「OracleのCPU_COUNTとOSのCPU数・OCPUは何が違う?」は、この記事の知識を前提にしています。あわせてどうぞ。
プロセスは「お店」、スレッドは「店員」
大将に聞くと、こう答えます。
大将「店を開くには、厨房と冷蔵庫とレジがいる。それで、働く人が最低1人いる」
これが、ほぼそのままプロセスの定義です。
Microsoft の「About Processes and Threads」では、
- プロセスは、プログラムを動かすのに必要な資源を提供するもの。仮想アドレス空間(そのプロセス専用のメモリの見え方)、実行するコード、開いているハンドル(ファイルなどを使うための引換券)、セキュリティの情報、プロセス ID、環境変数などを持ち、少なくとも1つのスレッドを持つ
- プロセスは1つのスレッド(プライマリスレッド)で始まり、そこから追加のスレッドを作れる
- スレッドは、プロセスの中で、実行の順番を割り当てられる(スケジュールされる)もの
- 同じプロセスのスレッドは、そのプロセスの仮想アドレス空間とシステムの資源を共有する
と説明されています。
うどん屋さんに置きかえると、こうです。
| コンピューターの言葉 | うどん屋さん |
|---|---|
| プロセス | お店(厨房・冷蔵庫・レジ・営業許可) |
| スレッド | そのお店で働く店員 |
| メモリ(変数) | 店の冷蔵庫と棚 |
| プロセス ID(PID) | お店の登録番号 |
| プライマリスレッド(Python ではメインスレッド) | 開店のときからいる最初の店員 |
新人さん「じゃあ、プロセスは何もしないんですか?」
大将「店は料理しない。料理するのは店員」
資料でいう「スケジュールされるのはスレッド」は、まさにこれです。CPU で実際に命令を実行してもらう順番が回ってくるのは、店ではなく店員のほうなんです。
新人さん「店員ゼロのお店は?」
大将「それは、ただの空き物件」
同じ店の店員は、冷蔵庫を共有する
いちばん大事な違いは、メモリを共有するかどうかです。
実際に、スレッドとプロセスで同じ関数を動かして、同じ変数(丼のリスト)に書き込ませてみます。
import os
import threading
from multiprocessing import Process
bowls = []
def add(who):
bowls.append(who)
print(f"{who}: pid={os.getpid()} 丼={bowls}", flush=True)
if __name__ == "__main__":
print(f"親 : pid={os.getpid()} 丼={bowls}", flush=True)
t = threading.Thread(target=add, args=("スレッド",))
t.start()
t.join()
p = Process(target=add, args=("プロセス",))
p.start()
p.join()
print(f"親 : pid={os.getpid()} 丼={bowls}", flush=True)
親 : pid=22756 丼=[]
スレッド: pid=22756 丼=['スレッド']
プロセス: pid=24204 丼=['プロセス']
親 : pid=22756 丼=['スレッド']
(今回の検証環境:Windows の Python 3.13.1 で python -X utf8 t1_share.py として実行。PID は実行するたびに変わります)
見るところは2つです。
- スレッドの PID は親と同じ 22756。同じお店の中の店員だからです。そしてスレッドが入れた丼は、親からも見えています
- プロセスの PID は 24204 で、別のお店。そこで入れた丼は、親の丼には増えていません
新人さん「プロセスの人、ちゃんと丼を置いたのに……」
大将「置いたよ。向こうの店の棚にね」
Python の threading のドキュメントにも、threading モジュールは1つのプロセスの中で動くので、すべてのスレッドが同じメモリ空間を共有する、と書かれています。
multiprocessing のドキュメントは、逆に、プロセスどうしでデータをやりとりするには、キューやパイプ(オブジェクトをシリアライズして送る)を使い、どうしても共有したいなら Value や Array の共有メモリを使う、と案内しています。共有の状態はできるだけ避けるのがよい、とも書かれています。
なお、if __name__ == "__main__": は飾りではありません。Windows の multiprocessing は、子プロセスに新しい Python を起動する spawn という方式が既定で、この書き方がないとうまく動かないことがあります(詳しくは実践編の記事で書きました)。
新人さん「別の店に丼を届けたいときは?」
おうどん「出前(キュー)を使う。棚に置いて念じても届かない」
PIDとスレッドの番号を確かめる
自分のプログラムがどのお店の、どの店員なのか。番号で確かめる方法です。
Python で確かめる
import os
import threading
def show(name):
print(f"{name}: pid={os.getpid()} native_id={threading.get_native_id()}", flush=True)
show("メインスレッド")
workers = [threading.Thread(target=show, args=(f"スレッド{i}",)) for i in (1, 2)]
for w in workers:
w.start()
for w in workers:
w.join()
メインスレッド: pid=28608 native_id=23284
スレッド1: pid=28608 native_id=14056
スレッド2: pid=28608 native_id=14076
PID は3人とも同じ、native_id は1人ずつ違います。
threading のドキュメントによると、get_native_id() はカーネル(OS の中心部分)が割り当てたスレッド ID で、スレッドが終わるまではシステム全体でそのスレッドを区別できます(終わったあとは再利用されることがあります)。
似た名前の get_ident() もありますが、こちらは「値そのものに直接の意味はない」番号です。OS のツールと見比べたいなら get_native_id() のほうを使いましょう。
新人さん「ident と native、どっちが本名ですか」
おうどん「native が戸籍。ident は店のまかない当番表の番号」
PowerShell で確かめる
Windows PowerShell 5.1 では、Get-Process でプロセスを見られます。$PID は、今の PowerShell 自身の PID が入っている自動変数です。
Get-Process -Id $PID | Select-Object Id, ProcessName, @{Name = 'Threads'; Expression = { $_.Threads.Count } } | Format-Table -AutoSize
Id ProcessName Threads
-- ----------- -------
28220 powershell 20
(今回の検証環境:Windows PowerShell 5.1.19041.7802。スレッドの数は環境やタイミングで変わります)
Threads は、Get-Process が返す Process オブジェクト(System.Diagnostics.Process)のプロパティで、そのプロセスのスレッドの一覧です。.Count で人数を数えています。
PowerShell 1つ、お店1軒に、店員が20人。
新人さん「何もしてないのに20人も?」
大将「開店準備の人、掃除の人、電話番。客がいなくても店は回ってる」
Get-Process を引数なしで打つと、ローカルの全部のプロセスが一覧で出ます。Get-Process のドキュメントによると、既定の表の Id 列が PID です。
🔰 ここまで読めば今日から困らない
- プロセスは「お店」(メモリやファイルなどの資源を持つ単位)、スレッドは「店員」(実際に実行の順番が回ってくる単位)
- 1つのプロセスには、スレッドが1本以上いる。最初の1本は開店のときからいる
- 同じプロセスのスレッドはメモリを共有する。別のプロセスの変数は、ふつう見えない
- PID は Python なら
os.getpid()、PowerShell なら$PIDやGet-Processで確かめる
新人さん「4行なら、まかないの時間に覚えられます」
大将「覚えたら、丼も洗ってね」
新人さん「PID がお店の登録番号なら、毎回同じ番号ですか?」
大将「うちは屋台方式。開くたびに番号が変わる」
さっきの実行例でも、スクリプトを動かすたびに PID は違う番号になっていました。
ここから先は中級者向け。読み飛ばしてもOKです。
共有の代償:丼の数が合わなくなる
冷蔵庫を共有できるのはスレッドの便利なところですが、そのぶん事故も起きます。
2人の店員が同時に「棚の丼を数えて、1つ足して、書き戻す」をすると、数がずれるんです。
import threading
import time
n = 0
lock = threading.Lock()
def unsafe():
global n
for _ in range(1000):
v = n
time.sleep(0) # ほかのスレッドに順番を譲る
n = v + 1
def safe():
global n
for _ in range(1000):
with lock:
v = n
time.sleep(0)
n = v + 1
for target in (unsafe, safe):
n = 0
ts = [threading.Thread(target=target) for _ in range(4)]
for t in ts:
t.start()
for t in ts:
t.join()
print(f"{target.__name__}: 4スレッド×1000回 → n = {n}")
unsafe: 4スレッド×1000回 → n = 1162
safe: 4スレッド×1000回 → n = 4000
(今回の検証環境で3回実行し、unsafe は 1162・1166・1146、safe は3回とも 4000 でした。上の出力は1回目です)
4人が1000回ずつ足したのに、1162。3000回近く消えています。
何が起きたかというと、
- 店員Aが棚を見る「丼は10個」
- 店員Bも棚を見る「丼は10個」
- Aが「11個」と書き戻す
- Bも「11個」と書き戻す
2人で1つずつ足したのに、11個。1回分が消えます。これを競合状態(レースコンディション。順番しだいで結果が変わってしまう状態)と言います。
店員A「11個にしておきました!」
店員B「私も11個にしておきました!」
2人とも正直で、2人とも間違っている。一番やっかいなタイプです。
time.sleep(0) は、今回わざとほかのスレッドに順番を回すために入れています。これがなくても、読む・足す・書き戻すが別々の手順になっている限り、ずれる可能性は残ります(ずれる頻度は環境しだいで、今回の検証では sleep なしの場合は試していません)。
対策は、threading.Lock() で「棚を触っていいのは1人ずつ」にすること。with lock: の中は、1人が終わるまで、ほかの店員は待ちます。safe のほうは3回とも 4000 でぴったりでした。
新人さん「じゃあ、全部の行を lock で囲めば安心ですね」
おうどん「全員が1本の菜箸を順番待ちする厨房になる。安全だけど、店員を増やした意味がない」
ロックで守るのは、共有のデータを読み書きする短い部分だけにするのが基本です。
プロセスなら、この事故は起きにくい
別のプロセスはメモリを共有しないので、「同じ変数を2人で書き換える」事故はふつう起きません。そのかわり、結果を集めるにはキューなどでやりとりする手間がかかります。Value などで共有メモリを使う場合は、プロセスでも同じ注意が必要です。
店員がこけても店は開いている。でも、店を閉める店員もいる
スレッドとプロセスでは、「1人が失敗したときの影響」も違います。3つ試してみます。
import os
import sys
import threading
from multiprocessing import Process
def boom():
raise ValueError("麺が切れました")
def leave():
os._exit(3)
if __name__ == "__main__":
mode = sys.argv[1]
if mode == "thread-exception":
t = threading.Thread(target=boom)
t.start()
t.join()
print("メインスレッド: まだ営業中です", flush=True)
elif mode == "thread-exit":
t = threading.Thread(target=leave)
t.start()
t.join()
print("メインスレッド: まだ営業中です", flush=True)
elif mode == "process-exit":
p = Process(target=leave)
p.start()
p.join()
print(f"親プロセス: 子の exitcode={p.exitcode}、まだ営業中です", flush=True)
1. スレッドで例外が起きた(python -X utf8 t4_crash.py thread-exception)
メインスレッド: まだ営業中です
標準エラーには次のトレースバックが出ました(最後の2行だけ載せています)。
raise ValueError("麺が切れました")
ValueError: 麺が切れました
終了コードは 0。スレッドの中の例外はそのスレッドで止まり、メインスレッドは何事もなかったように営業を続けました。
新人さん「店員さんがこけたのに、店は平常運転なんですね」
大将「こけた店員の分の注文は、誰も作ってないけどね」
ここが落とし穴です。スレッドでエラーが起きても、プログラム全体は成功(終了コード 0)で終わることがあります。スレッドに任せた仕事が本当に終わったかは、結果を受け取る仕組みを作って確かめましょう。
2. スレッドが os._exit(3) を呼んだ(thread-exit)
(何も表示されない。終了コードは 3)
(これは出力ではなく説明です。標準出力・標準エラーとも空でした)
「まだ営業中です」は出ませんでした。os._exit() は、プロセスそのものを終わらせる関数です。店員1人が、店ごと閉めて帰ってしまったわけです。
3. 子プロセスが os._exit(3) を呼んだ(process-exit)
親プロセス: 子の exitcode=3、まだ営業中です
別のお店が閉店しても、親のお店は営業中。子の終了コード 3 は、exitcode で受け取れました。
新人さん「店員1人が店ごと閉められるの、権限強すぎません?」
おうどん「同じ店の中にいるって、そういうこと」
同じプロセスの中のスレッドは、良くも悪くも一蓮托生。プロセスを分けると、片方が倒れても、もう片方は残ります。
スレッドとプロセス、どっちを使う?
ここまでの違いを並べると、こうなります。
| スレッド | プロセス | |
|---|---|---|
| メモリ | 同じプロセスの中で共有 | ふつうは別々 |
| データのやりとり | 変数をそのまま読める(競合に注意) | キュー・パイプ・共有メモリ |
| 作る手間 | 軽め | 重め(Windows の spawn は Python を新しく起動する) |
| 1つが落ちたとき | 例外ならそのスレッドだけ。プロセスを終わらせる操作なら全部 | ほかのプロセスは残る |
| Python(CPython)で CPU の計算を並列に | GIL のため速くなりにくい | 複数の CPU を使える |
Python の threading のドキュメントには、CPython では GIL(グローバルインタープリターロック。Python のコードを同時に実行できるスレッドを1本にする仕組み)のため、一度に Python のコードを実行できるのは1スレッドだけ、と書かれています。複数コアを活かしたいなら multiprocessing や concurrent.futures.ProcessPoolExecutor がすすめられていて、I/O 待ち(ファイルやネットワークの待ち時間)の多い処理を同時に動かすなら、スレッドは今でも適したモデルだ、とされています。
ざっくり言うと、
- 待ち時間が多い仕事(ダウンロード、DB の応答待ち)→ スレッド
- 計算ばかりの仕事(大量のログの集計)→ プロセス
です。
新人さん「待ちながら計算もする仕事は?」
おうどん「注文を待ちながら麺も打つ店だね。それは処理を分けて考える。まず、どっちの時間が長いかを測る」
冒頭の新人さんの質問の答えは、「その処理が待っているのか、計算しているのかによる」になります。
新人さん「えっ、答えが質問で返ってきた」
おうどん「基礎講座の答えは、だいたいそういうもの」
実際に測った結果は、実践編の「Pythonのログ集計を並列化しても速くならない原因と対処」に載せています。
ここから先は上級者向け。読み飛ばしてもOKです。
CPUの「4スレッド」と、プログラムのスレッドは別物
パソコンの仕様に「2コア4スレッド」と書いてあることがあります。この「スレッド」と、ここまでの「スレッド」は、同じ言葉で別のものを指しています。
今回の検証環境で、CPU の数を確かめました。
Get-CimInstance -ClassName Win32_Processor | Select-Object NumberOfCores, NumberOfLogicalProcessors | Format-Table -AutoSize
NumberOfCores NumberOfLogicalProcessors
------------- -------------------------
2 4
import os
import sys
import threading
print(sys.version.split()[0])
print("cpu_count:", os.cpu_count())
print("process_cpu_count:", os.process_cpu_count())
print("active_count:", threading.active_count())
3.13.1
cpu_count: 4
process_cpu_count: 4
active_count: 1
- 「2コア4スレッド」の4は、論理プロセッサ(OS から見た CPU の数)。うどん屋さんで言えば、同時に使えるコンロの口の数
- プログラムのスレッドは、そのコンロを使う店員の数
Microsoft の資料には、Windows はプリエンプティブ・マルチタスク(OS が順番を割り込んで切り替える方式)で複数のスレッドが同時に動いているように見せ、複数のプロセッサがあるコンピューターでは、プロセッサの数だけのスレッドを同時に実行できる、と書かれています。
店員が20人いても、コンロが4口なら、同時に茹でられるのは4人まで。残りは交代で使います。
新人さん「うちのパソコンは4スレッドだから、Python のスレッドも4本までですか?」
大将「店員の人数に上限の張り紙はないよ。コンロの前で並ぶだけ」
さっきの PowerShell が、4口のコンロで20人の店員を抱えていたのが、その証拠です。
新人さん「じゃあ、店員を4人にそろえれば最強ですね」
大将「待ち時間に洗い物をする店員もいるから、そう単純でもない」
Python の os のドキュメントでは、os.cpu_count() はシステムの論理 CPU の数、os.process_cpu_count()(3.13 で追加)は今のプロセスの呼び出し元スレッドが使える論理 CPU の数で、CPU アフィニティ(使ってよい CPU の指定)によっては cpu_count() より少なくなることがある、とされています。並列の数を決めるときは、後者のほうが実際に近い値です。
Oracle の CPU_COUNT と OS の CPU 数の関係は、実践編の「OracleのCPU_COUNTとOSのCPU数・OCPUは何が違う?」にまとめました。
Linuxでは、スレッドも同じPIDを名乗る
ここまでは Windows で確かめましたが、Linux でも「同じプロセスのスレッドは PID を共有する」のは同じです。ただ、そこに至るまでには歴史がありました。
Linux の pthreads(7) によると、
- 同じプロセスのスレッドは、同じグローバルメモリ(データとヒープの領域)を共有し、スタックはスレッドごとに持つ
- POSIX.1 は、プロセス ID、開いているファイルディスクリプター、シグナルの扱い、資源の制限などを、スレッドではなくプロセス全体の属性として共有することを求めている
- スレッド ID、シグナルマスク、
errnoなどはスレッドごとに別 - 古い実装の LinuxThreads では、
getpid()がスレッドごとに違う値を返していた - 今の実装の NPTL では、プロセスのすべてのスレッドが同じスレッドグループに入り、同じ PID を共有する
と書かれています。
同じ店の店員なのに、登録番号は1人ずつ違う。大将に言わせれば、それはもう別の店です。
昔の Linux では、店員ごとに登録番号が違って見えていたわけです。古い資料を読むときは、この違いに気をつけましょう(今回の検証環境に Linux はないので、ここは資料で確かめた範囲です)。
Pythonのプロセスの始め方も、OSと版で違う
multiprocessing のドキュメントによると、子プロセスの始め方(スタートメソッド)は次のとおりです。
| 方式 | 中身 | 既定になる環境 |
|---|---|---|
| spawn | 新しい Python のプロセスを起動し、run() に必要な資源だけ引き継ぐ。fork や forkserver より起動がかなり遅い | Windows と macOS |
| forkserver | Linux など、Unix パイプでファイルディスクリプターを渡せる POSIX で使える | 3.14 以降、それらの POSIX の既定。3.15 では、書き込める一時ディレクトリがないときは spawn |
Windows の spawn は、毎回「新しいお店を一から開いている」ようなものです。のれん分けのたびに、店舗を新築している。
ドキュメントにも遅いと書かれているくらいなので、短い仕事を細かく子プロセスに投げると、起動の手間が目立ちやすくなります。
GILのないPythonも出てきている
threading のドキュメントには、Python 3.13 から free-threaded(フリースレッド)版のビルドで GIL を無効にでき、スレッドを本当に並列に動かせる、ただし既定では使えない(PEP 703)、とも書かれています。
「Python のスレッドは CPU の計算では速くならない」は、今の通常のビルド(今回の検証環境の 3.13.1 もこちら)での話です。free-threaded 版は、今回の検証環境では試していません。
じゃあ、その版なら丼の数もずれないのかというと、逆です。本当に同時に動くぶん、ロックはもっと大事になります。
GIL という門番がいなくなった厨房で、店員が全員いっせいに棚へ走る。想像するだけで丼が割れそうです。
GIL があってもなくても、前の章の「数えて、足して、書き戻す」は別々の手順です。共有データはロックで守る、は変わりません。
まとめ:お店を増やすか、店員を増やすか
冒頭の新人さんの質問に戻ります。
新人さん「スレッドを増やすか、プロセスを増やすか。わかりました。店員を増やすか、お店を増やすか、ですね」
おうどん「そう。店員は冷蔵庫を共有できるけど、取り合いになる。お店は冷蔵庫が別だけど、出前の手間がかかる」
新人さん「で、この処理は……待ってばかりなので、店員を増やします!」
大将「ロックの菜箸も、1本用意しておいてね」
新人さん「ついでに、店員を100人雇っておきます!」
大将「コンロは4口だよ。99人が厨房の入口で渋滞する」
人数は、測ってから決めましょう。
プロセスは資源を持つお店、スレッドはそこで実行を受け持つ店員。この1行があれば、並列処理の記事も、Oracle の PROCESSES も、CPU の「4スレッド」も、ぐっと読みやすくなります。
まずは PowerShell で Get-Process -Id $PID を打って、自分の PowerShell のお店の登録番号を見てみてください。
自分の店の番号を知っていると、ほかの店の看板も読みやすくなります🍜
参考資料
- Microsoft Learn:About Processes and Threads — プロセスが持つ資源、プライマリスレッド、スレッドはスケジュールされる単位で仮想アドレス空間を共有すること、プリエンプティブ・マルチタスクとプロセッサの数だけ同時に実行できること
- Python ドキュメント:threading — スレッドは同じメモリ空間を共有すること、GIL と I/O 待ちに向くこと、get_native_id と get_ident、active_count、3.13 の free-threaded ビルド
- Python ドキュメント:multiprocessing — サブプロセスで GIL を避けること、キューとパイプ、共有の状態は避ける・Value と Array、spawn と forkserver の既定、Process.pid
- Python ドキュメント:os — os.getpid、os.cpu_count と os.process_cpu_count の違い
- Microsoft Learn:Get-Process(PowerShell 5.1) — -Id と $PID、Process オブジェクトを返すこと、既定の表示の Id 列が PID
- Linux man-pages:pthreads(7) — スレッドが共有する属性とスレッドごとの属性、LinuxThreads と NPTL での PID の違い
2026年10月時点の内容です。

コメント