はじめに
「Pythonでマルチスレッドにしたのに速くならない...」
その原因は GIL(Global Interpreter Lock) 。
これを理解してないと、Pythonの並列処理でハマるよ。
GILとは?
GIL = グローバルインタプリタロック
Pythonインタプリタが一度に1つのスレッドしかPythonバイトコードを実行しないようにするロック。
┌───────────────────────────────────────┐
│ Python Interpreter │
│ ┌─────────────────────────────────┐ │
│ │ GIL 🔒 │ │
│ │ ここを通れるのは1スレッドだけ │ │
│ └─────────────────────────────────┘ │
│ │
│ Thread1 ──→ 待機 │
│ Thread2 ──→ 実行中 🏃 │
│ Thread3 ──→ 待機 │
│ Thread4 ──→ 待機 │
└───────────────────────────────────────┘
実際に計測してみる
CPU負荷タスク
def cpu_bound_task(n: int) -> int:
"""CPU負荷の高い計算"""
count = 0
for i in range(n):
count += i * i
return count
シングルスレッド
# 4回順番に実行
cpu_bound_task(10_000_000)
cpu_bound_task(10_000_000)
cpu_bound_task(10_000_000)
cpu_bound_task(10_000_000)
# => 3.07秒
マルチスレッド
threads = []
for _ in range(4):
t = threading.Thread(target=cpu_bound_task, args=(10_000_000,))
threads.append(t)
t.start()
for t in threads:
t.join()
# => 2.93秒 ← 速くなってない!
4スレッドなのにほぼ同じ時間! これがGILの影響。
I/O負荷なら速くなる
GILはI/O待ちの間解放される。
def io_bound_task(seconds: float):
time.sleep(seconds) # I/O待ち中はGILを解放
結果
| 方式 | 時間 |
|---|---|
| シングルスレッド | 2.00秒 |
| マルチスレッド | 0.50秒 ← 4倍速! |
I/O待ち(ネットワーク、ファイル読み書き、sleep)では並行処理が効く!
なぜGILがあるの?
CPythonのメモリ管理
CPythonは参照カウントでメモリ管理している:
a = [] # refcount = 1
b = a # refcount = 2
del a # refcount = 1
del b # refcount = 0 → 解放
マルチスレッドで参照カウントを同時に更新すると壊れる。
GILは参照カウントを守るための単純な解決策。
なぜ今も残ってる?
- 取り除くとシングルスレッド性能が落ちる
- C拡張との互換性が壊れる
- 歴史的経緯(最初からあった)
GILの回避方法
1. multiprocessing(プロセス分離)
from multiprocessing import Pool
def cpu_bound(n):
return sum(i * i for i in range(n))
with Pool(4) as pool:
results = pool.map(cpu_bound, [10_000_000] * 4)
# プロセスごとに別のGIL → 真の並列実行
2. concurrent.futures
from concurrent.futures import ProcessPoolExecutor
with ProcessPoolExecutor(max_workers=4) as executor:
futures = [executor.submit(cpu_bound, 10_000_000) for _ in range(4)]
results = [f.result() for f in futures]
3. Cython / C拡張
GILを明示的に解放できる:
with nogil:
# ここではGILなしで実行
heavy_computation()
4. NumPy / SciPy
内部でGILを解放してる:
import numpy as np
# NumPyの演算は内部でGILを解放して並列実行
result = np.dot(large_matrix1, large_matrix2)
5. asyncio(I/O向け)
async def fetch_all():
await asyncio.gather(
fetch_url("https://api1.example.com"),
fetch_url("https://api2.example.com"),
fetch_url("https://api3.example.com"),
)
使い分けチャート
タスクの種類は?
│
├─ CPU負荷(計算、画像処理など)
│ │
│ └─ multiprocessing / ProcessPoolExecutor
│
└─ I/O負荷(ネットワーク、ファイルなど)
│
├─ 同期的 → threading / ThreadPoolExecutor
│
└─ 非同期 → asyncio
Python 3.13+ の変化
Free-threaded Python(実験的)
Python 3.13からGILなしのビルドが実験的に利用可能:
# GILなしPythonのインストール(将来)
python3.13t # t = thread-safe
まだ実験段階だが、将来的にはGILが消えるかも?
まとめ
| 状況 | 解決策 |
|---|---|
| CPU負荷の並列化 | multiprocessing |
| I/O負荷の並行化 |
threading or asyncio
|
| 数値計算 | NumPy(内部でGIL解放) |
| 最大パフォーマンス | Cython, C拡張 |
覚えること
- GIL = Pythonは1スレッドしか同時実行できない
- CPU負荷 → スレッドでは速くならない
- I/O負荷 → スレッドでも速くなる
- CPU並列化したいなら multiprocessing
これでGIL完全理解!🎄