Python のマルチスレッドとマルチプロセスの使い分け
Python で並列処理を行う方法として、マルチスレッドとマルチプロセスがあります。それぞれに特徴があり、処理の内容によって使い分けが重要です。
基本的な違い
マルチスレッド
同じプロセス内でメモリを共有。GIL により同時に1スレッドだけが Python コードを実行。I/O バウンドな処理に適している。
マルチプロセス
独立したプロセスでメモリは分離。真の並列実行が可能。CPU バウンドな処理に適している。
GIL の影響
Python の GIL(Global Interpreter Lock)により、マルチスレッドでは CPU バウンドな処理の並列化ができません。
import threading
import multiprocessing
import time
def cpu_bound(n):
"""CPU を使う重い計算"""
total = 0
for i in range(n):
total += i * i
return total
N = 10_000_000
# シングルスレッド
start = time.time()
cpu_bound(N)
cpu_bound(N)
print(f"シングル: {time.time() - start:.2f}秒")
# マルチスレッド(GIL のため速くならない)
start = time.time()
t1 = threading.Thread(target=cpu_bound, args=(N,))
t2 = threading.Thread(target=cpu_bound, args=(N,))
t1.start(); t2.start()
t1.join(); t2.join()
print(f"マルチスレッド: {time.time() - start:.2f}秒")
# マルチプロセス(真の並列化)
start = time.time()
p1 = multiprocessing.Process(target=cpu_bound, args=(N,))
p2 = multiprocessing.Process(target=cpu_bound, args=(N,))
p1.start(); p2.start()
p1.join(); p2.join()
print(f"マルチプロセス: {time.time() - start:.2f}秒")I/O バウンド処理にはスレッド
ファイル I/O やネットワーク通信など、待ち時間が多い処理にはスレッドが適しています。
from concurrent.futures import ThreadPoolExecutor
import urllib.request
import time
def download(url):
response = urllib.request.urlopen(url)
return len(response.read())
urls = ["https://example.com"] * 5
start = time.time()
with ThreadPoolExecutor(max_workers=5) as executor:
results = list(executor.map(download, urls))
print(f"スレッド: {time.time() - start:.2f}秒")I/O 待ち中は GIL が解放されるため、スレッドが効果的です。
CPU バウンド処理にはプロセス
数値計算や画像処理など、CPU を使う処理にはプロセスが適しています。
from concurrent.futures import ProcessPoolExecutor
import time
def compute(n):
return sum(i * i for i in range(n))
numbers = [5_000_000] * 4
start = time.time()
with ProcessPoolExecutor(max_workers=4) as executor:
results = list(executor.map(compute, numbers))
print(f"プロセス: {time.time() - start:.2f}秒")
print(f"結果: {results}")使い分けの判断基準
| 処理の種類 | 推奨 | 理由 |
|---|---|---|
| ファイル読み書き | スレッド | I/O 待ち中に他のスレッドが動ける |
| ネットワーク通信 | スレッド | I/O 待ちが多い |
| 数値計算 | プロセス | CPU を使うので真の並列化が必要 |
| 画像処理 | プロセス | CPU バウンド |
| データ変換 | プロセス | CPU バウンド |
メモリ共有の違い
import threading
import multiprocessing
shared_list = []
def append_thread(item):
shared_list.append(item) # 同じリストを共有
def append_process(item):
shared_list.append(item) # 別プロセスなので反映されない
# スレッド
t = threading.Thread(target=append_thread, args=("thread",))
t.start(); t.join()
print(f"スレッド後: {shared_list}") # ['thread']
# プロセス
p = multiprocessing.Process(target=append_process, args=("process",))
p.start(); p.join()
print(f"プロセス後: {shared_list}") # ['thread'](変わらない)プロセス間でデータを共有するには、Queue や Manager を使う必要があります。
作成コストの違い
スレッド
作成コストが低い。メモリ効率が良い。大量に作成しても問題ない。
プロセス
作成コストが高い。メモリを多く消費。作成数を抑えるべき。
組み合わせて使う
I/O と CPU の両方がある場合は、組み合わせることもあります。
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
import urllib.request
def download(url):
response = urllib.request.urlopen(url)
return response.read()
def process_data(data):
# CPU を使う処理
return len(data) * 2
# ダウンロード(I/O)はスレッドで
with ThreadPoolExecutor(max_workers=5) as executor:
downloaded = list(executor.map(download, ["https://example.com"] * 5))
# データ処理(CPU)はプロセスで
with ProcessPoolExecutor(max_workers=4) as executor:
results = list(executor.map(process_data, downloaded))まとめ
スレッドを選ぶ場合
I/O バウンドな処理。軽量な並行処理。データ共有が多い場合。
プロセスを選ぶ場合
CPU バウンドな処理。真の並列化が必要。メモリ分離が必要。
処理の特性を理解して、適切な並列化手法を選びましょう。











