ストレス:16コアのマシンなのにPythonが1コアしか使わない?
16コア32スレッドの超高性能ワークステーションを購入したとしましょう。意気揚々とデータ処理スクリプトを実行したものの、タスクマネージャーを見ると悲しい現実が。たった1つのCPUコアだけがフル稼働し、残りは「遊んでいる」状態です。なぜでしょうか?
主な原因は、CPythonのGlobal Interpreter Lock (GIL)です。このメカニズムにより、マルチコア上で複数のスレッドが同時にPythonコードを実行することが制限されています。このボトルネックから抜け出すには、ThreadingとMultiprocessingの違いを正しく理解する必要があります。ツールを間違えると、コードが遅くなるだけでなく、システムリソースの無駄遣いにも繋がります。
イメージ化:レストランとシェフ
あなたが大規模な厨房を運営していると想像してみてください:
- Threading(マルチスレッド): 4本の腕を持つ1人のシェフ。肉を焼きながらスープの様子を見ることはできますが、脳は1つしかないため、交互に注意を向ける必要があります。複雑な計算(数学の問題など)を解く必要がある場合、手を止めて考え込まなければなりません。
- Multiprocessing(マルチプロセス): 4人のシェフを雇い、それぞれ独立した4つの厨房に配置します。各自が専用のまな板、包丁、コンロを持ち、完全に並列で作業します。もし1人が誤って火傷(クラッシュ)しても、残りの3人は通常通り料理を提供し続けられます。
1. Threading:待機時間の多いタスクの救世主
Pythonにおいて、Threadingは計算を高速化するものではありません。これはI/Oバウンドな課題を解決するために生まれました。I/Oバウンドとは、CPUがハードディスクからのデータ読み込み、APIからのレスポンス待ち、データベースの結果待ちなど、大半の時間を「待機」に費やすタスクのことです。
スレッド1がサーバーからのレスポンスを待っている間、GILが解放され、スレッド2が処理を開始できます。これにより、CPUに負荷をかけずに全体の実行時間を大幅に短縮できます。
2. Multiprocessing:真のパワー
16コアのCPUを最大限に活用したいなら、Multiprocessingを使いましょう。各プロセスは独自のPythonインタプリタとメモリ領域を持ちます。これにより、GILの影響を完全に回避できます。画像処理、ビデオエンコード、大規模な行列計算などのCPUバウンドなタスクにおいて、ナンバーワンの選択肢となります。
実戦:数字は嘘をつかない
2つの代表的なシナリオで効率を比較してみましょう。
シナリオ1:100個のウェブサイトからデータをスクレイピングする(I/Oバウンド)
逐次実行(シングルスレッド)では約50〜60秒かかりますが、Threadingを使えば5〜7秒まで短縮可能です。
import threading
import requests
import time
# 100個のURLリストをシミュレート
urls = ["https://google.com"] * 100
def fetch_url(url):
requests.get(url, timeout=5)
def run_threading():
threads = []
for url in urls:
t = threading.Thread(target=fetch_url, args=(url,))
threads.append(t)
t.start()
for t in threads: t.join()
if __name__ == "__main__":
start = time.time()
run_threading()
print(f"Threading完了時間: {time.time() - start:.2f}秒")
注意: ここでMultiprocessingを使わないでください。100個のプロセスを起動すると、ウェブサイトのレスポンスを待つだけのために数GBのRAMを消費してしまいます。
シナリオ2:1GBのログファイルを処理する(CPUバウンド)
1GBのログファイルを処理する場合、正規表現(Regex)を使って数百万行のログから情報を抽出することを考えます。このとき、CPUは100%の出力で稼働する必要があります。
import multiprocessing
import time
def heavy_computation(data_chunk):
# 二乗和の計算による重い処理のシミュレーション
return sum(i * i for i in range(10**7))
if __name__ == "__main__":
tasks = [1, 2, 3, 4]
# マルチコアで実行
start = time.time()
with multiprocessing.Pool(processes=4) as pool:
pool.map(heavy_computation, tasks)
print(f"Multiprocessing(4コア)の所要時間: {time.time() - start:.2f}秒")
Core i7のマシンでは、Multiprocessingを使用することで、通常のforループによる実行と比較して時間を約4分の1に短縮できます。
ヒント: 複雑な正規表現を扱う際は、事前に Regex Tester でパターンを確認することをお勧めします。マルチプロセスのデバッグはシングルスレッドよりもはるかに大変なため、並列処理に組み込む前にロジックのミスを防ぐことが重要です。
クイック選択表(チートシート)
| 特徴 | Threading | Multiprocessing |
|---|---|---|
| メモリ領域 | 共有(RAMを節約) | 独立(RAMをより多く消費) |
| 通信 | 容易(変数を直接共有) | 複雑(IPCやQueueが必要) |
| 安定性 | 1つのスレッドの失敗がアプリ全体に影響 | 1つのプロセスが終了しても他に影響しない |
| 推奨用途 | API呼び出し、ファイル読み書き、DBクエリ | 数値計算、画像処理、AI/ML |
実務経験から得た3つの教訓
- プロセス数を過剰に増やさない: 8コアのCPUで100個のプロセスを作成しないでください。OSが頻繁にコンテキストスイッチを行うことになり、パフォーマンスが低下するだけでなく、マシンが非常に重くなります。
- 共有状態(Shared State)に注意: Threadingにおいて、2つのスレッドが同じグローバル変数を変更すると競合状態(Race Condition)が発生します。機密データの保護には必ず
threading.Lock()を使用してください。 - 使用するライブラリ:
concurrent.futuresは、コードを一行変えるだけで ThreadからProcessに切り替えられる現代的なライブラリです。古いモジュールよりもコードが綺麗になり、管理も容易です。
結びに
最高のツールというものはなく、最適なツールがあるだけです。アプリが「待機」に時間を使っているならThreadingを、アプリが「思考(計算)」を必要としているならMultiprocessingを選びましょう. GILの本質を理解することは、よりプロフェッショナルで効率的なPythonコードを書くための大きな助けとなります。

