🐥

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バウンド マルチプロセス 数値計算、データ処理
軽量処理 シングルスレッド 単純なスクリプト

重要なポイント

  1. PythonのGILは1プロセス内で1スレッドしか実行できない

    • 4コアCPUでも、マルチスレッドでは最大25%のCPU使用率
    • I/O待機中は他のスレッドに切り替わるため、I/Oバウンド処理では効果的
  2. マルチプロセスはGILを回避できる

    • 各プロセスが独立したGILを持つ
    • CPUコア数に応じてCPU使用率を100%近くまで上げられる
    • メモリ使用量とプロセス起動コストがトレードオフ
  3. 非同期は大量のI/O接続に最適

    • 1スレッドで数千の接続を効率的に処理
    • スレッド/プロセスよりメモリ効率が良い

最後に

今回はPythonにおけるスレッド・プロセス・非同期の概要と使い分けに関して解説しました。
これらの特性を理解して処理に応じてスレッド、プロセス、非同期を使い分けましょう。

Discussion