threading と multiprocessing — スレッド・プロセス・GIL

スレッドとプロセスの違い、GILでCPUバウンドが並列にならない理由、ThreadPoolExecutorとmultiprocessing.Pool、subprocessの外部コマンド呼び出しと使い分けフローを図解で整理します。

Python の並行・並列処理 — threading(スレッド)multiprocessing(プロセス)、そしてsubprocess(外部コマンド)を整理します。ブラウザ仮想環境の Python では実スレッド・実プロセスが動かないため、本記事は図解と read-only の code ブロックで概念を押さえる形になります。

ブラウザでは実行できない

本記事のcodeブロックは実機の Python で動かす想定の例示です。threading.Threadstart()multiprocessing.Poolmap()は、ブラウザのサンドボックス内ではOS スレッド・OS プロセスが作れないため動きません。代わりに概念整理のクイズを末尾に用意しています。

threading / multiprocessing / subprocess の使いどころ
threadingI/O バウンドファイル / DB / APImultiprocessingCPU バウンド画像処理 / 数値計算subprocess外部コマンドgit / ffmpeg / シェルasyncio (前記事)I/O 多数並行Web API 100 件
threadingは I/O 待ちが支配的な処理、multiprocessingは CPU を使い切る計算、subprocessは Python 外のコマンド呼び出しに使う。asyncioで扱った I/O 多数並行と並べると役割の違いがはっきりする。

プロセスとスレッドの違い

プロセスOS が割り当てる「実行単位」で、独立したメモリ空間を持ち、互いに干渉しません。一方スレッド1 つのプロセスの中の軽量な実行単位で、同じメモリを共有します。共有しているからこそデータ受け渡しが速い反面、競合状態(race condition、複数スレッドが同じ変数を同時に書き換えて結果が壊れる現象)に注意する必要があります。

「プロセス = 重いが独立、スレッド = 軽いが共有」と覚えておくとよいです。

プロセス・スレッド・コルーチンの入れ子
プロセス (multiprocessing が増やす)
  • OS から見た独立した実行単位
  • メモリ独立GIL の制約なし
  • 起動コスト大、真の並列が効く
スレッド (threading が増やす)
  • プロセス内でコードを動かす単位
  • メモリ共有GIL の制約あり
  • I/O バウンドで効果が出る
コルーチン (asyncio が動かす)
  • 1 スレッド内で切り替えながら動く
  • さらに軽量、I/O 多数並行に強い
multiprocessingが増やすのは一番外のプロセスthreadingが増やすのは中段のスレッドasyncioが動かすのは一番内側のコルーチン。3 つは別々の階層を扱うので、何を並列化したいかで選び分ける。
プロセスとスレッドの違い
プロセス(multiprocessing)メモリ独立起動コスト大干渉なし真の並列スレッド(threading)メモリ共有起動軽量競合に注意GIL の制約あり
プロセスメモリ独立で起動コストが高い代わりに干渉ゼロ、スレッドメモリ共有で軽いが同期(Lock など)が要る。Python の場合はGILの制約があり、CPU バウンドではスレッドの並列性能が出ない。

threading と GIL — Python のスレッドの制約

Python(CPython)にはGIL(Global Interpreter Lock)という仕組みがあり、「同時に 1 つのスレッドだけが Python のバイトコードを実行できる」という制約があります。CPU の重い処理だと、マルチスレッドで並行に走らせても、実質シングルスレッドと変わらない速度になってしまいます。

GIL — 同時に動けるのは 1 スレッドだけ
Thread A計算中GILA が保持Thread B待機Thread AI/O 待ち→ GIL 解放GILB が取得Thread B計算開始受け渡し
Python バイトコードを実行する権利は GIL という 1 つのロックが握る。複数スレッドは順番待ちしながら動き、I/O 待ちの瞬間に GIL を手放すのでその間は別のスレッドが進める。

一方、I/O バウンド(処理時間が外部応答の待ち時間で決まる処理 — ネットワーク・ファイル・DB の応答待ち)の間はGIL を手放すので、I/O バウンドな処理は threading で並行化する効果が出ます。ただ、I/O バウンドならasyncio のほうがオーバーヘッドが小さく書きやすいことが多いので、新規コードでは asyncio での記述を優先しましょう

GIL がスレッドに与える影響
CPU バウンド(計算処理)threadingGIL で順番待ち→ 並列にならないmultiprocessing が必要I/O バウンド(API / DB / ファイル)threadingI/O 待ちで GIL 解放→ 並行に進む(asyncio が更に軽量)
CPU バウンドな処理はスレッドを増やしても GIL で順番待ちになり並列にならない。I/O バウンドは I/O 待ち中に GIL を手放すので、threading で効果が出る。真の並列が要る CPU バウンドには multiprocessing。
# threading: 低レベル API
import threading

def worker(name):
    print(f"{name} 開始")
    # 何か処理
    print(f"{name} 完了")

t1 = threading.Thread(target=worker, args=("A",))
t2 = threading.Thread(target=worker, args=("B",))
t1.start()
t2.start()
t1.join()  # 完了を待つ
t2.join()

# concurrent.futures: 高レベル API (こちらが推奨)
from concurrent.futures import ThreadPoolExecutor

def fetch(url):
    # 実際は requests.get(url) など I/O バウンド処理
    return f"fetched: {url}"

with ThreadPoolExecutor(max_workers=4) as executor:
    urls = ["a.com", "b.com", "c.com"]
    results = list(executor.map(fetch, urls))
    print(results)

新規コードは ThreadPoolExecutor を選ぶ

素のthreading.Threadを直接使うとライフサイクル管理が煩雑になります。concurrent.futures.ThreadPoolExecutorwith で安全に管理でき、executor.map(関数, リスト)でリスト全体を並行に処理できる API です。スレッド数の上限max_workers)も指定できるので、サーバーへの過度な接続を防ぐのにも便利です。

マルチスレッドを使うとよい場面

threading / ThreadPoolExecutorが向くのは待ち時間を別タスクで埋めたい場面です。

- 複数の Web API / DB / ファイル I/O を並行に処理したいとき

- 既存の同期ライブラリ(async 非対応)を並行化したいとき

- subprocess で起動した複数プロセスの出力を並行に読み取りたいとき

- GUI のメインループを止めずにバックグラウンド処理をしたいとき

multiprocessing — 真の並列

multiprocessing複数の Python プロセスを起動して並列に動かすモジュールです。プロセスはGIL の制約から解放されるので、CPU バウンドな処理を真の並列で動かせます — 4 コアの CPU なら、画像処理 / 数値計算が約 4 倍速くなります。

multiprocessing.Pool — 4 コアで真の並列
入力[d1, d2, d3, d4]Process 1Core 1heavy(d1)Process 2Core 2heavy(d2)Process 3Core 3heavy(d3)Process 4Core 4heavy(d4)結果[r1, r2, r3, r4]
4 つの Python プロセスを別々の CPU コアで動かすため、GIL の制約がなく真の並列実行になる。CPU バウンドな処理が約 4 倍速くなる。
from multiprocessing import Pool

def heavy(n):
    return sum(i * i for i in range(n))   # CPU バウンド処理

if __name__ == "__main__":   # multiprocessing の必須形
    with Pool(processes=4) as pool:
        results = pool.map(heavy, [10**6, 10**6, 10**6, 10**6])
        print("sum:", sum(results))

multiprocessing は if __name__ == '__main__': 必須

multiprocessing子プロセスが親と同じスクリプトを再実行する仕組みのため、トップレベルでいきなり Pool(...).map(...) を呼ぶと無限再帰で爆発します。Windows / macOS のspawn方式では特に厳格で、メインのコードは必ず if __name__ == "__main__": ブロックの中に書きます

subprocess — 外部コマンド

subprocessPython から外部コマンド(OS のシェルコマンド)を呼び出すためのモジュールです — git statusの実行、ffmpegで動画変換、シェルスクリプトの呼び出しなど、「Python 以外のプログラムを起動する」用途に使います。multiprocessing と名前は似ていますが、まったく別物です。

subprocess.run — Python から外部コマンドを呼ぶ
Pythonsubprocess.run([...])OS別プロセスを起動外部コマンドgit / ffmpeg 等stdout / returncodeを Python に返す
Python が OS にコマンド実行を依頼別の OS プロセスとして外部コマンドが走り → 標準出力と returncode が CompletedProcess に入って戻る。Python の中で書ける範囲を超えた処理を任せる用途。
import subprocess

result = subprocess.run(
    ["git", "status", "--short"],
    capture_output=True,
    text=True,
    check=True,    # 失敗したら CalledProcessError
)
print(result.stdout)
print("return code:", result.returncode)

判断フロー — どれを選べばよいか

asyncio / threading / multiprocessing / subprocessの使い分けは、「処理が CPU バウンドか I/O バウンドか」「Python 内か外部コマンドか」の 2 軸で決まります。下のフロー図に従えば、迷うことはほぼ無くなります。

並行・並列処理の判断フロー
外部コマンドを呼ぶ?I/O バウンド?(待ち時間が支配的)CPU バウンド?(計算が支配的)→ subprocess→ asyncio(or threading)→ multiprocessingYesYesYes
外部コマンドなら subprocessI/O バウンドなら asyncio (or threading)CPU バウンドなら multiprocessing。3 つの軸で使い分ける。
処理の性質推奨理由
Web API を 100 件並行に叩くasyncioI/O バウンド、軽量で書きやすい
既存の同期 HTTP クライアントを並行化threading (ThreadPoolExecutor)ライブラリが async 非対応なら threading
画像処理を 4 コアで並列化multiprocessingCPU バウンドは GIL を回避するためにプロセス
git / ffmpeg などのコマンド実行subprocessPython 外のプログラムを呼ぶ専用
数百万件の単純な計算ループNumPy / CythonPython レベルの並列化より、ベクトル化のほうが速い

「真に CPU バウンド」は意外と少ない

Python で「計算で詰まっている」ように見える処理の多くは、実はNumPy / Pandas / Cython でベクトル化すれば 100 倍速くなるケースが珍しくありません。multiprocessing で 4 倍を狙う前に、数値計算は NumPy、データ処理は Pandas、文字列処理は正規表現の最適化など、「並列化より先にやるべきこと」をまず確認します。

QUIZ

理解度チェック

まずは1問ずつ答えてみましょう。

Q1Python の GIL(Global Interpreter Lock)が原因で、スレッドを増やしても並列にならないのはどんな処理ですか?

Q2Python から git status などの外部コマンドを呼ぶときに使うのはどれですか?

Q34 コアの CPU で重い数値計算を真に並列化したいときに最も向いているのはどれですか?