Pythonのマルチスレッド・マルチプロセス・非同期の使い分け
はじめに
この記事では、Pythonにおけるスレッドとプロセスの違いと、それぞれの適切な使い分けについて解説します。PythonにはGIL(Global Interpreter Lock)が存在し、並行処理の設計に大きな影響を与えます。
プロセスとスレッドの基本
プロセスとは
実行中のプログラムの単位で、以下のようなリソースを管理しており独立したメモリ空間を持ちます:
- コードセグメント: プログラムの命令が格納される読み取り専用の領域
- データセグメント: プログラム開始時から終了まで存在する変数の領域(グローバル変数、クラス変数、定数など)
- ヒープ: 実行時に動的に確保・解放されるメモリ領域(オブジェクト生成時に使用)
- スタック: 関数の実行に必要な一時的なデータを格納する領域(ローカル変数、引数、戻り先アドレスなど)
- レジスタ: CPU内部の超高速メモリ
プロセス間通信はデータのコピー/送信等の処理が発生するため遅くなります。
スレッドとは
プロセス内の実行単位です。同一プロセス内のメモリを共有するため、スレッド間の通信は高速です。ただし、瞬間的には1コアだけを使用し、時間経過で別のコアに移動可能です。

PythonのGIL(Global Interpreter Lock)
GILとは
PythonのGIL(Global Interpreter Lock)は、同時に実行できるスレッドを1つに制限する仕組みです。これはPythonインタープリタのメモリ管理を簡単にするために実装されています。
メリット:
- スレッドセーフな実装が容易
- データ競合によるバグを防止
- シンプルなメモリ管理
デメリット:
- マルチコアCPUの性能を十分に活用できない
- CPU集約的な処理では並列化の効果が得られない
GILによるCPU使用率の制限
4コアCPUの環境でPythonのマルチスレッドプログラムを実行した場合、GILにより同時に実行できるスレッドは1つだけに制限されます。そのため、本来4つの物理コアを利用したいですが、実際には1つの物理コアしか利用できず、システム全体でみるとCPU使用率は最大25%となります。

この図が示すように:
- プロセス1のスレッド1はCore-1で実行中
- Core-2、Core-3、Core-4は使用されず(FREE)
- 4コア中1コアのみ使用 = CPU使用率25%
これがPythonのGILによる大きな制約です。
マルチスレッド
仕組み
Pythonのthreadingモジュールを使用した並行処理では、複数のスレッドが1つのプロセス内で実行されますが、GILにより同時に実行できるのは1スレッドのみです。
適している処理: I/Oバウンドなタスク
有効なケース:
- API呼び出し、HTTP通信
- ファイルの読み書き
- データベースアクセス
- ネットワーク通信
I/O待機時間中に他のスレッドが処理を進められるため、全体的な処理時間を短縮できます。

この図が示すように:
シングルスレッド:
- API 1実行 → 待機 → API 2実行 → 待機 → API 3実行 → 待機
- 合計: 9秒
マルチスレッド:
- スレッド1: API 1実行 → 待機(この間スレッド2,3が実行) → API 1完了
- スレッド2: 待機 → API 2実行 → 待機 → API 2完了
- スレッド3: 待機 → API 3実行 → 待機 → API 3完了
- 合計: 3秒
I/O待機中に他のスレッドが実行されるため、並行処理の効果が得られます。
import threading
import requests
def call_api(url):
response = requests.get(url)
return response.json()
# 複数のAPIを並行呼び出し
urls = ['https://api1.com', 'https://api2.com', 'https://api3.com']
threads = []
for url in urls:
thread = threading.Thread(target=call_api, args=(url,))
threads.append(thread)
thread.start()
# 全スレッドの完了を待機
for thread in threads:
thread.join()
不向きな処理: CPUバウンドなタスク
CPU集約的な計算処理では、GILにより同時実行が制限されるため、シングルスレッドと同程度の性能になります。スレッド切り替えのオーバーヘッドにより、むしろパフォーマンスが劣化することもあります。
マルチプロセス
仕組み
multiprocessingモジュールを使用すると、各プロセスが独立したGILを持つため、真の並列実行が可能になります。複数のCPUコアを最大限活用できます。
適している処理: CPUバウンドなタスク
有効なケース:
- 数値計算、科学計算
- データ変換、集計処理
from multiprocessing import Pool
import os
def heavy_computation(n):
print(f"Process {os.getpid()} processing {n}")
result = sum(i * i for i in range(n))
return result
if __name__ == '__main__':
# 4つのプロセスで並列処理
with Pool(processes=4) as pool:
numbers = [10000000, 20000000, 30000000, 40000000]
results = pool.map(heavy_computation, numbers)
print(f"Results: {results}")
トレードオフ
メリット:
- 複数のCPUコアを完全に活用可能
- GILの制約を回避
- 各プロセスが独立したGILを持つため真の並列実行が可能
デメリット:
- プロセス起動のオーバーヘッドが大きい
- メモリ使用量が増加(各プロセスが独立したメモリ空間を持つ)
- プロセス間通信が必要(Queue、Pipe等)
並行処理の選択フローチャート
1. I/O待機が多いか?
YES → マルチスレッド or 非同期I/O
- API呼び出し、ファイルI/O、DB接続など
-
threadingモジュールまたはasyncioを使用
NO → 次へ
2. CPU集約的な処理か?
YES → マルチプロセス
- 数値計算、データ処理など
-
multiprocessingモジュールを使用
NO → シングルスレッドで十分
3. メモリ制約が厳しいか?
YES → マルチスレッド
- メモリ共有が可能なスレッドが有利
NO → マルチプロセス
- より高い並列性能を実現
非同期処理という選択肢
Python 3.4以降では、asyncioを使った非同期プログラミングが可能です。
特徴
- 1つのスレッドで数千以上の接続を効率的に処理
- イベントループベースの非同期実行
- メモリ効率が良い
使い分け
| 同時接続数 | 推奨手法 | 理由 |
|---|---|---|
| 少数(〜100) | マルチスレッド | シンプルで十分 |
| 大量(1000+) | 非同期 | リソース効率が高い |
import asyncio
import aiohttp
async def fetch_data(session, url):
async with session.get(url) as response:
return await response.json()
async def main():
urls = ['https://api1.com', 'https://api2.com', 'https://api3.com']
async with aiohttp.ClientSession() as session:
tasks = [fetch_data(session, url) for url in urls]
results = await asyncio.gather(*tasks)
return results
# 実行
asyncio.run(main())
メリット
- 1スレッドで数千の接続を処理可能
- メモリ効率が良い
- スレッド切り替えのオーバーヘッドなし
- I/O待機中に他のタスクを実行
注意点
- CPU集約的な処理には不向き
- async/awaitの学習コストあり
- 既存のライブラリが非対応の場合あり
まとめ
| 処理タイプ | 推奨手法 | 主な用途 |
|---|---|---|
| I/Oバウンド(少数) | マルチスレッド | API呼び出し、ファイルI/O |
| I/Oバウンド(大量) | 非同期 | Webサーバー、チャットアプリ |
| CPUバウンド | マルチプロセス | 数値計算、データ処理 |
| 軽量処理 | シングルスレッド | 単純なスクリプト |
重要なポイント
-
PythonのGILは1プロセス内で1スレッドしか実行できない
- 4コアCPUでも、マルチスレッドでは最大25%のCPU使用率
- I/O待機中は他のスレッドに切り替わるため、I/Oバウンド処理では効果的
-
マルチプロセスはGILを回避できる
- 各プロセスが独立したGILを持つ
- CPUコア数に応じてCPU使用率を100%近くまで上げられる
- メモリ使用量とプロセス起動コストがトレードオフ
-
非同期は大量のI/O接続に最適
- 1スレッドで数千の接続を効率的に処理
- スレッド/プロセスよりメモリ効率が良い
最後に
今回はPythonにおけるスレッド・プロセス・非同期の概要と使い分けに関して解説しました。
これらの特性を理解して処理に応じてスレッド、プロセス、非同期を使い分けましょう。
Discussion