Q1Python の GIL(Global Interpreter Lock)が原因で、スレッドを増やしても並列にならないのはどんな処理ですか?
threading と multiprocessing — スレッド・プロセス・GIL
スレッドとプロセスの違い、GILでCPUバウンドが並列にならない理由、ThreadPoolExecutorとmultiprocessing.Pool、subprocessの外部コマンド呼び出しと使い分けフローを図解で整理します。
Python の並行・並列処理 — threading(スレッド)とmultiprocessing(プロセス)、そしてsubprocess(外部コマンド)を整理します。ブラウザ仮想環境の Python では実スレッド・実プロセスが動かないため、本記事は図解と read-only の code ブロックで概念を押さえる形になります。
ブラウザでは実行できない
本記事のcodeブロックは実機の Python で動かす想定の例示です。threading.Threadのstart()やmultiprocessing.Poolのmap()は、ブラウザのサンドボックス内ではOS スレッド・OS プロセスが作れないため動きません。代わりに概念整理のクイズを末尾に用意しています。
プロセスとスレッドの違い
プロセスはOS が割り当てる「実行単位」で、独立したメモリ空間を持ち、互いに干渉しません。一方スレッドは1 つのプロセスの中の軽量な実行単位で、同じメモリを共有します。共有しているからこそデータ受け渡しが速い反面、競合状態(race condition、複数スレッドが同じ変数を同時に書き換えて結果が壊れる現象)に注意する必要があります。
「プロセス = 重いが独立、スレッド = 軽いが共有」と覚えておくとよいです。
- OS から見た独立した実行単位
- メモリ独立・GIL の制約なし
- 起動コスト大、真の並列が効く
- プロセス内でコードを動かす単位
- メモリ共有・GIL の制約あり
- I/O バウンドで効果が出る
- 1 スレッド内で切り替えながら動く
- さらに軽量、I/O 多数並行に強い
threading と GIL — Python のスレッドの制約
Python(CPython)にはGIL(Global Interpreter Lock)という仕組みがあり、「同時に 1 つのスレッドだけが Python のバイトコードを実行できる」という制約があります。CPU の重い処理だと、マルチスレッドで並行に走らせても、実質シングルスレッドと変わらない速度になってしまいます。
一方、I/O バウンド(処理時間が外部応答の待ち時間で決まる処理 — ネットワーク・ファイル・DB の応答待ち)の間はGIL を手放すので、I/O バウンドな処理は threading で並行化する効果が出ます。ただ、I/O バウンドならasyncio のほうがオーバーヘッドが小さく書きやすいことが多いので、新規コードでは asyncio での記述を優先しましょう。
# threading: 低レベル API
import threading
def worker(name):
print(f"{name} 開始")
# 何か処理
print(f"{name} 完了")
t1 = threading.Thread(target=worker, args=("A",))
t2 = threading.Thread(target=worker, args=("B",))
t1.start()
t2.start()
t1.join() # 完了を待つ
t2.join()
# concurrent.futures: 高レベル API (こちらが推奨)
from concurrent.futures import ThreadPoolExecutor
def fetch(url):
# 実際は requests.get(url) など I/O バウンド処理
return f"fetched: {url}"
with ThreadPoolExecutor(max_workers=4) as executor:
urls = ["a.com", "b.com", "c.com"]
results = list(executor.map(fetch, urls))
print(results)
新規コードは ThreadPoolExecutor を選ぶ
素のthreading.Threadを直接使うとライフサイクル管理が煩雑になります。concurrent.futures.ThreadPoolExecutorはwith で安全に管理でき、executor.map(関数, リスト)でリスト全体を並行に処理できる API です。スレッド数の上限(max_workers)も指定できるので、サーバーへの過度な接続を防ぐのにも便利です。
マルチスレッドを使うとよい場面
threading / ThreadPoolExecutorが向くのは待ち時間を別タスクで埋めたい場面です。
- 複数の Web API / DB / ファイル I/O を並行に処理したいとき
- 既存の同期ライブラリ(async 非対応)を並行化したいとき
- subprocess で起動した複数プロセスの出力を並行に読み取りたいとき
- GUI のメインループを止めずにバックグラウンド処理をしたいとき
multiprocessing — 真の並列
multiprocessingは複数の Python プロセスを起動して並列に動かすモジュールです。プロセスはGIL の制約から解放されるので、CPU バウンドな処理を真の並列で動かせます — 4 コアの CPU なら、画像処理 / 数値計算が約 4 倍速くなります。
from multiprocessing import Pool
def heavy(n):
return sum(i * i for i in range(n)) # CPU バウンド処理
if __name__ == "__main__": # multiprocessing の必須形
with Pool(processes=4) as pool:
results = pool.map(heavy, [10**6, 10**6, 10**6, 10**6])
print("sum:", sum(results))
multiprocessing は if __name__ == '__main__': 必須
multiprocessingは子プロセスが親と同じスクリプトを再実行する仕組みのため、トップレベルでいきなり Pool(...).map(...) を呼ぶと無限再帰で爆発します。Windows / macOS のspawn方式では特に厳格で、メインのコードは必ず if __name__ == "__main__": ブロックの中に書きます。
subprocess — 外部コマンド
subprocessはPython から外部コマンド(OS のシェルコマンド)を呼び出すためのモジュールです — git statusの実行、ffmpegで動画変換、シェルスクリプトの呼び出しなど、「Python 以外のプログラムを起動する」用途に使います。multiprocessing と名前は似ていますが、まったく別物です。
import subprocess
result = subprocess.run(
["git", "status", "--short"],
capture_output=True,
text=True,
check=True, # 失敗したら CalledProcessError
)
print(result.stdout)
print("return code:", result.returncode)
判断フロー — どれを選べばよいか
asyncio / threading / multiprocessing / subprocessの使い分けは、「処理が CPU バウンドか I/O バウンドか」と「Python 内か外部コマンドか」の 2 軸で決まります。下のフロー図に従えば、迷うことはほぼ無くなります。
| 処理の性質 | 推奨 | 理由 |
|---|---|---|
| Web API を 100 件並行に叩く | asyncio | I/O バウンド、軽量で書きやすい |
| 既存の同期 HTTP クライアントを並行化 | threading (ThreadPoolExecutor) | ライブラリが async 非対応なら threading |
| 画像処理を 4 コアで並列化 | multiprocessing | CPU バウンドは GIL を回避するためにプロセス |
| git / ffmpeg などのコマンド実行 | subprocess | Python 外のプログラムを呼ぶ専用 |
| 数百万件の単純な計算ループ | NumPy / Cython | Python レベルの並列化より、ベクトル化のほうが速い |
「真に CPU バウンド」は意外と少ない
Python で「計算で詰まっている」ように見える処理の多くは、実はNumPy / Pandas / Cython でベクトル化すれば 100 倍速くなるケースが珍しくありません。multiprocessing で 4 倍を狙う前に、数値計算は NumPy、データ処理は Pandas、文字列処理は正規表現の最適化など、「並列化より先にやるべきこと」をまず確認します。
理解度チェック
まずは1問ずつ答えてみましょう。
Q2Python から git status などの外部コマンドを呼ぶときに使うのはどれですか?
Q34 コアの CPU で重い数値計算を真に並列化したいときに最も向いているのはどれですか?