Pythonの並行処理と並列処理 完全ガイド:シングルスレッドからマルチコアへの進化
まず根本的な疑問に答える:なぜPythonにはこれほど多くの並行処理手段があるのか
Javaを使ってきた人は、疑問に感じるかもしれません。JavaではThread + ExecutorService + CompletableFutureという一つの道筋で進められるのに、なぜPythonにはthreading、multiprocessing、asyncio、concurrent.futures、さらにはconcurrent.interpretersまであるのでしょうか。
理由は、PythonがJavaにはない歴史的な制約、GIL(Global Interpreter Lock) を抱えているからです。このロックによってPythonのマルチスレッドはCPU計算を真に並列実行できないため、Pythonコミュニティは、この制約を回避または解消する複数の手段を発展させてきました。それぞれに適した用途があり、互いを置き換えるのではなく、補い合う関係にあります。
この記事では最も基礎的な概念から始め、それぞれの手段が何か、なぜ存在するのか、どの場面に適するのか、将来どのように進化するのかを段階的に説明します。
第1章:並行処理と並列処理——二つの異なる概念
日常会話では、この二つの言葉が混同されることがよくあります。しかし、プログラミングでは厳密に異なる意味を持ちます。ここを混同すると、以降の内容がすべて曖昧になります。
並行処理(Concurrency)
並行処理とは、複数のタスクが時間的に重なりながら進むこと です。ただし、必ずしも同じ瞬間に実行されているとは限りません。
前回の料理人のたとえを続けましょう。一人の料理人が三つの料理を同時に進めます。三種類の食材を本当に同時に切るのではありません。魚を切り終えて漬け込んでいる間に野菜を炒め、野菜を鍋に入れた後の空き時間にソースを作ります。三つの料理は同じ時間帯に進みますが、どの瞬間を見ても、料理人が行っている作業は一つだけです。
並行処理の中核的な価値は、待ち時間を効率よく活用すること です。ネットワークリクエスト、ファイルの読み書き、データベースクエリといったI/Oバウンドなタスクに特に適しています。この種のタスクは、ほとんどの時間を外部からの応答待ちに費やし、その間CPUは実質的に暇だからです。
並列処理(Parallelism)
並列処理とは、複数のタスクが同じ瞬間に本当に同時に実行されること です。そのためには、複数のCPUコアなど複数の実行単位が必要です。
同じ厨房のたとえなら、四人の料理人を雇い、それぞれに一つずつコンロを与えます。四つの料理を物理的な意味で本当に同時に炒めます。
並列処理の中核的な価値は、計算時間を短縮すること です。データ処理、科学技術計算、画像レンダリングといったCPUバウンドなタスクに特に適しています。この種のタスクには待ち時間がほとんどなく、CPUは常に動いています。高速化するには、複数のコアで同時に計算する必要があります。
Javaとの比較
JavaのThreadは本来、並列処理に対応しています。JVMのスレッドはOSレベルのスレッドであり、複数のスレッドを異なるコア上で動かし、Javaバイトコードを真に同時実行できます。そのためJavaでは、マルチスレッドは並行処理にも並列処理にも使える手段です。
PythonではGILが存在するため、状況ははるかに複雑です。Pythonコードについては、threadingは並行処理を実現できても並列処理は実現できません。並列実行にはmultiprocessingなど別の手段が必要です。Pythonの並行処理ツール群がJavaより複雑なのは、このためです。
第2章:GIL——Pythonの並行処理に関するあらゆる問題を理解する鍵
GILは、CPythonインタープリター内にあるグローバルな排他ロックです。その規則は極めて単純です。ある瞬間に、Pythonプロセス全体でPythonバイトコードを実行できるスレッドは一つだけです。
GILが存在する理由
CPythonは、オブジェクトのメモリ上のライフサイクルを管理するために参照カウントを使用します。各Pythonオブジェクトの内部には、そのオブジェクトを指す参照の数を記録するカウンターがあります。カウントがゼロになると、オブジェクトは直ちに解放されます。
a = [1, 2, 3] # 列表对象的引用计数 = 1
b = a # 引用计数 = 2
del a # 引用计数 = 1
del b # 引用计数 = 0 → 对象被释放
参照カウントの増減はアトミックな操作ではありません。二つのスレッドが同じオブジェクトの参照カウントを同時に変更すると、競合状態が発生します。たとえば、スレッドAがdel aを実行してカウントを一つ減らすのと同時に、スレッドBがc = aによってカウントを一つ増やす場合です。最悪の場合、カウントが誤ってゼロになり、オブジェクトが早すぎるタイミングで解放されます。参照をまだ保持しているスレッドが解放済みメモリへアクセスし、セグメンテーションフォルトを引き起こします。
GILは、この問題を最も単純で強制的な方法で解決しました。参照カウントの操作が安全でないなら、常に一つのスレッドだけがPythonコードを実行するよう保証します。これによって参照カウントの変更は自然に直列化され、各オブジェクトへ個別にロックを追加する必要がなくなります。
GILがもたらす結果
I/Oバウンドなタスクには、ほとんど影響しません。 スレッドがsocket.recv()などのI/O操作を行うとき、CPythonはシステムコールへ入る前に 自らGILを解放し、ほかのスレッドへ実行の機会を与えます。I/Oが完了すると、再びGILを取得します。そのため、マルチスレッドによるWebクローラーや並行HTTPリクエストなどは期待どおり機能します。
CPUバウンドなタスクには致命的です。 複数のスレッドが、数値計算やリスト操作といった純粋なPython処理を実行すると、GILを奪い合い、細かい単位で直列実行されます。さらに、GILの取得と解放そのものにもコストがあるため、CPUバウンドな処理をマルチスレッド化すると、シングルスレッドより遅くなる場合さえあります。
Javaの視点から考えるなら、JVMに一つのグローバルロックがあり、どのスレッドもJavaコードを1行実行するたびに、そのロックを先に取得しなければならない状況を想像してください。10個のコア上で10個のスレッドを動かしても、同じ瞬間にJavaコードを実行できるのは一つだけです。これがGILによってPythonに生じる効果です。
GILについての補足
見落とされがちな点として、GILがロックするのは Pythonバイトコード の実行だけです。C拡張が純粋なCコードを実行する際に自らGILを解放すれば、そのCコードはほかのスレッドと真に並列実行できます。Pythonから呼び出したNumPyの行列演算がマルチコアを利用できるのは、このためです。NumPyの中核的な計算はCレベルで実行され、その間GILが解放されます。
第3章:Pythonにおける四つの並行・並列処理手段
GILを理解すると、Pythonの四つの手段がそれぞれどこに当てはまるかが明確になります。
手段1:threading——GILの下で行う協調的なマルチスレッド
threadingモジュールは、OSレベルのスレッドを提供します。各スレッドは実在するOSスレッドであり、OSによってスケジュールされます。ただしGILがあるため、同じ瞬間にPythonバイトコードを実行できるスレッドは一つだけです。
適した場面:I/Oバウンドな並行処理。 100件のHTTPリクエストを同時に送る、複数のファイルを同時に読み書きする、複数のデータベースクエリの応答を同時に待つ、といった処理です。I/Oを待っている間はGILが解放され、ほかのスレッドが実行できます。
適さない場面:CPUバウンドなタスク。 純粋なPython計算をマルチスレッドで実行しても、シングルスレッドより速くならず、かえって遅くなることがあります。
import threading
import requests
def fetch_url(url):
response = requests.get(url)
print(f"{url}: {response.status_code}")
urls = ["https://httpbin.org/delay/1"] * 5
# 创建五个线程,每个线程发一个请求
threads = [threading.Thread(target=fetch_url, args=(url,)) for url in urls]
for t in threads:
t.start() # 启动线程
for t in threads:
t.join() # 等待所有线程完成
# 五个请求并发执行,总耗时约 1 秒而不是 5 秒
# 因为在等待 HTTP 响应时,GIL 被释放,其他线程可以运行
Javaのマルチスレッドとの重要な違い: JavaのマルチスレッドはJavaコードを真に並列実行できます。PythonのthreadingはPythonコードを実行するとき、見かけ上の並列動作にとどまります。ただし、I/O待ちを処理する効果は両者で似ています。
手段2:multiprocessing——マルチプロセスでGILを回避する
GILがプロセス単位のロックなら、最も直接的な回避策は複数のプロセスを起動することです。各プロセスには、独自のPythonインタープリター、独自のGIL、独自のメモリ空間があります。複数のプロセスはPythonコードを真に並列実行できます。
適した場面:CPUバウンドな並列計算。 データ処理、科学技術計算、画像の一括処理などです。
コスト: プロセス間ではメモリを共有しません。データを渡すにはpickleによるシリアライズとデシリアライズが必要であり、スレッド間通信よりはるかに大きなコストがかかります。プロセス自体を作成するコストも、スレッドより大きくなります。
from multiprocessing import Pool
def heavy_computation(n):
"""一个 CPU 密集的计算"""
return sum(i * i for i in range(n))
# 创建一个包含 4 个 worker 进程的进程池
with Pool(4) as pool:
# 把任务分发到 4 个进程并行执行
results = pool.map(heavy_computation, [10_000_000] * 4)
# 4 个进程各自有独立的 GIL,可以真正并行计算
# 总耗时约为单进程的 1/4(在 4 核 CPU 上)
Javaにたとえると: JavaのProcessBuilderで子プロセスを起動することにやや似ています。ただし、Pythonのmultiprocessingはより使いやすく抽象化され、APIもthreadingと非常によく似ているため、切り替えの負担は小さくなっています。
手段3:asyncio——シングルスレッドのイベント駆動型並行処理
asyncioは、前の記事で扱ったシングルスレッド、イベントループ、コルーチン、awaitから成る体系です。それ自体はマルチスレッドやマルチプロセスをまったく使用せず、一つのスレッド内で協調的なスケジューリングを行うことで並行処理を実現します。
適した場面:大規模なI/O並行処理、特にネットワークI/O。 数千から数万のネットワーク接続を同時に管理する場合、asyncioはthreadingより効率的です。スレッドを作成・切り替えるコストもGILの競合もないためです。
適さない場面:CPUバウンドなタスク。ただし、スレッドプールまたはプロセスプールと組み合わせる場合を除きます。
import asyncio
import aiohttp
async def fetch_url(session, url):
async with session.get(url) as response:
return await response.text()
async def main():
async with aiohttp.ClientSession() as session:
# asyncio.gather 让多个协程并发执行
tasks = [fetch_url(session, f"https://httpbin.org/delay/1") for _ in range(100)]
results = await asyncio.gather(*tasks)
# 100 个请求并发执行,总耗时约 1-2 秒
asyncio.run(main())
Javaにたとえると: asyncioは概念的にはJava NIO(Non-blocking I/O)やCompletableFutureに似ています。より正確には、NettyフレームワークのEventLoopモデルに近いものです。Java 21のVirtual Threads(Project Loom)も発想がよく似ています。多数の軽量な並行実行単位を協調的にスケジュールし、効率のよいI/O並行処理を実現します。
asyncioとthreadingの本質的な違い:
threadingは プリエンプティブ です。いつスレッドを切り替えるかはOSが決めるため、コードはどの位置でも中断される可能性があります。この性質によって競合状態のリスクが生じ、共有データをロックで保護する必要があります。
asyncioは 協調的 です。明示的にawaitを書いた場所でのみ制御を手放します。二つのawaitの間では、ほかのコルーチンがコードを中断することはありません。この性質によって、並行プログラミングの認知負荷が大きく下がります。「この行の実行途中に別のコルーチンが割り込むかもしれない」と心配する必要がありません。
ただし、この長所は制約でもあります。あるコルーチンが二つのawaitの間で大量の計算を行うと、ほかのすべてのコルーチンは待たされます。
手段4:concurrent.futures——統一された高レベルインターフェース
concurrent.futuresは、マルチスレッドとマルチプロセスに対するPythonの 高レベルな抽象化 です。統一されたExecutorインターフェースを提供し、ThreadPoolExecutor(スレッドプール)とProcessPoolExecutor(プロセスプール)を簡単に切り替えられます。場合によっては1行を変更するだけです。
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
def task(n):
return sum(i * i for i in range(n))
# I/O 密集型 → 用线程池
with ThreadPoolExecutor(max_workers=10) as executor:
futures = [executor.submit(task, 1000) for _ in range(10)]
results = [f.result() for f in futures]
# CPU 密集型 → 改一行就切换到进程池
with ProcessPoolExecutor(max_workers=4) as executor:
futures = [executor.submit(task, 10_000_000) for _ in range(4)]
results = [f.result() for f in futures]
Javaとの直接的な対応: concurrent.futuresはPython版のjava.util.concurrentです。PythonのExecutorはJavaのExecutorServiceに、FutureはJavaのFuture<T>に対応します。ThreadPoolExecutorとProcessPoolExecutorは、JavaのExecutors.newFixedThreadPool()などのファクトリーメソッドに対応します。JavaのExecutorServiceをよく理解していれば、このモジュールもすぐに習得できます。
第4章:どれを選ぶか——判断の流れ
具体的なタスクを前にしたとき、どの手段を選べばよいのでしょうか。次の流れで考えられます。
第1段階:タスクがI/OバウンドかCPUバウンドかを判断する。
I/Oバウンドとは、ネットワークリクエスト、ファイルの読み書き、データベースクエリのように、外部からの応答待ちに大半の時間を使うタスクです。CPUバウンドとは、データ処理、数値計算、画像処理のように、計算に大半の時間を使うタスクです。実際のタスクには両方が混在するものも多いため、ボトルネックがどこにあるかを判断する必要があります。
第2段階:I/Oバウンドなタスクの場合。
並行数が数十程度なら、threadingまたはThreadPoolExecutorで十分です。単純で直接的に使えます。並行数が数百から数千と大きい場合は、asyncioを使います。高並行I/Oでは消費するリソースが少なくなります。ただし、asyncioを使うには、呼び出すライブラリも非同期処理へ対応している必要があります。requestsではなくaiohttp、psycopg2ではなくasyncpgが必要です。依存ライブラリに同期版しかない場合は、threadingのほうが現実的です。
第3段階:CPUバウンドなタスクの場合。
multiprocessingまたはProcessPoolExecutorを使います。現時点で最も成熟した手段です。数値計算については、まずNumPyやPandasなどのライブラリを検討してください。中核的な処理をCレベルで実行してGILを解放するため、マルチスレッドと自然に連携できます。
第4段階:混合型タスクの場合。
複数の手段を組み合わせられます。たとえばWebアプリケーションのリクエスト処理は、データベースや下流APIを待つI/Oバウンドな処理なので、メインループにasyncioを使います。その一部にCPUバウンドな計算がある場合は、asyncio.to_thread()でスレッドプールへ送るか、loop.run_in_executor(ProcessPoolExecutor(), ...)でプロセスプールへ送ります。
第5章:asyncioとthreadingを実務で連携させる
前の記事では、iterate_in_threadpoolの仕組みを説明しました。ここでは、asyncioプログラムから同期ブロッキングコードを呼び出す、より一般的な場面を補足します。
asyncio.to_thread():最も簡単な橋渡し
Python 3.9では、asyncioプログラムから同期関数を安全に呼び出すためのasyncio.to_thread()が導入されました。
import asyncio
import time
def blocking_io():
"""模拟一个同步阻塞的 I/O 操作"""
time.sleep(2)
return "data from slow API"
async def main():
# 错误做法:直接在协程里调用阻塞函数
# result = blocking_io() # 这会冻结事件循环 2 秒!
# 正确做法:把阻塞函数丢到线程池
result = await asyncio.to_thread(blocking_io)
# 事件循环不会被阻塞,其他协程可以正常运行
asyncio.run(main())
内部の仕組みはiterate_in_threadpoolとまったく同じです。ブロッキング処理をスレッドプールで実行し、awaitでスレッドプールの結果を待ちます。その間、イベントループのスレッドには影響しません。
loop.run_in_executor():より柔軟な方法
スレッドプールやプロセスプールの具体的な構成を制御する必要がある場合や、CPUバウンドなタスクをプロセスプールで動かす必要がある場合は、run_in_executor()を使用できます。
import asyncio
from concurrent.futures import ProcessPoolExecutor
def cpu_heavy(n):
"""CPU 密集任务"""
return sum(i * i for i in range(n))
async def main():
loop = asyncio.get_running_loop()
# 用进程池执行 CPU 密集任务
with ProcessPoolExecutor(max_workers=4) as pool:
result = await loop.run_in_executor(pool, cpu_heavy, 10_000_000)
print(result)
asyncio.run(main())
この組み合わせは、実際の開発で非常によく使われます。特にFastAPIやStarletteのような非同期Webフレームワークで一般的です。
第6章:三つの手段の内部構造を比較する
理解を深めるため、低レベルの実行構造という観点から比較します。
threadingの実行モデル
threading.Threadを作成すると、CPythonはPOSIX pthreadまたはWindows Thread APIを通じて、実在するOSスレッドを作成します。複数のOSスレッドは、OSレベルでは実際に異なるCPUコアへスケジュールされます。しかし、各スレッドはPythonバイトコードを実行する前にGILを取得しなければなりません。Python 3.2以降の実装では、GILの取得と解放が約5ミリ秒ごとに切り替わり、各スレッドに実行の機会が与えられます。
したがって、OSの視点ではPythonのマルチスレッドは本当のマルチスレッドです。しかし、Pythonバイトコードの実行という視点ではGILによって直列化されています。I/O操作には影響しない一方でCPU計算を並列化できない理由は、ここにあります。I/O待ちの間はGILが解放されるからです。
multiprocessingの実行モデル
multiprocessing.Processを作成すると、CPythonはLinuxではfork、WindowsとmacOSではspawnによって、まったく新しいOSプロセスを作成します。新しいプロセスは、完全なPythonインタープリターのインスタンス、独自のGIL、独自のメモリ空間を持ちます。複数のプロセスは、異なるコア上でPythonバイトコードを真に並列実行できます。
その代わり、プロセス間通信にはパイプ(Pipe)、キュー(Queue)、共有メモリといったIPCの仕組みが必要です。プロセス間で渡すデータは通常pickleでシリアライズし、受信側でデシリアライズする必要があります。大量のデータを渡す場合、このコストは大きくなる可能性があります。
asyncioの実行モデル
asyncioは追加のスレッドもプロセスもまったく作成しません。一つのスレッド内でイベントループを動かします。コルーチンはPythonレベルの軽量なオブジェクトであり、OSスレッドではありません。コルーチンの切り替えに必要なのはPythonスタックフレームの保存と復元だけで、マイクロ秒単位のごく小さなコストです。一方、スレッドの切り替えはOSレベルのコンテキストスイッチであり、通常は数マイクロ秒から数十マイクロ秒かかります。
そのため、asyncioなら数万の並行コルーチンを容易に管理できますが、数万のスレッドを作成することはOSにとって大きな負担になります。
第7章:スレッド安全性とよくある落とし穴
GILがあってもスレッド安全とは限らない
「GILがあるため、Pythonのマルチスレッドではスレッド安全性を考える必要がない」という誤解は非常によく見られます。しかし、これは誤りです。
GILが保証するのは「同じ瞬間にバイトコードを実行するスレッドは一つだけ」ということです。一つのPython文が複数のバイトコード命令に対応する場合、任意の二つの命令の間でスレッドが切り替わる可能性があります。
import threading
counter = 0
def increment():
global counter
for _ in range(1_000_000):
counter += 1
# counter += 1 实际上是三步:
# 1. 读取 counter 的值(LOAD_GLOBAL)
# 2. 加 1(BINARY_ADD)
# 3. 写回 counter(STORE_GLOBAL)
# 线程切换可能发生在这三步的任何间隙
threads = [threading.Thread(target=increment) for _ in range(2)]
for t in threads:
t.start()
for t in threads:
t.join()
print(counter)
# 结果几乎不可能是 2,000,000
# 因为两个线程会互相覆盖对方的写入
正しい方法はロックを使うことです。
lock = threading.Lock()
def increment_safe():
global counter
for _ in range(1_000_000):
with lock: # 获取锁
counter += 1 # 这三步字节码在锁的保护下原子执行
# 离开 with 块时自动释放锁
asyncioの「安全という錯覚」
asyncioの協調的なスケジューリングでは、二つのawaitの間でコードが中断されないため、多くの競合状態がなくなります。しかし、二つのコルーチンがawaitの前後で同じ共有状態を読み書きすると、やはり競合状態が発生する可能性があります。
balance = 100
async def withdraw(amount):
global balance
current = balance # 读
await asyncio.sleep(0) # 这里让出控制权!另一个协程可能插进来
balance = current - amount # 写——但 current 可能已经过期了
# 如果两个协程同时 withdraw(80),可能都读到 balance=100
# 然后各自写回 20,最终 balance=20 而不是应该的 -60 或拒绝
したがって、asyncioでも「awaitの前に読み、awaitの後に書く」処理は、asyncio.Lockで保護する必要があります。
第8章:Pythonにおける並行処理の未来
Pythonの並行処理エコシステムは、この10年で最大の変革を迎えています。三つの方向で進化が進んでいます。
方向1:Free-threaded Python(GILなしビルド)
最も注目を集めている変化です。Python 3.13では、PEP 703によって実験的なfree-threadedビルドが初めて導入されました。Python 3.14では実験段階を終え、正式にサポートされるビルドオプションになっています。ただし、まだデフォルトのビルドではありません。
Free-threaded Pythonの中核的な変更は、GILを削除し、マルチスレッドでPythonバイトコードを真に並列実行できるようにすること です。GILなしでも参照カウントを安全に保つため、CPythonチームは「偏向参照カウント」(biased reference counting)という方式を実装しました。オブジェクトを作成したスレッドは高速な非アトミック操作で参照カウントを更新し、ほかのスレッドはアトミック操作を使います。また、dict、list、setなどの組み込み型にも、並行変更を保護する細粒度の内部ロックが追加されています。
現状では、free-threadedビルドはインストール時の特定のオプションで有効にする必要があります。インストール後の実行ファイルには、通常python3.14tのようにtの接尾辞が付きます。デフォルトの動作ではありません。Python 3.14では、シングルスレッドコードの性能低下が約5〜10%まで縮小しました。3.13の約40%から大幅に改善しています。
AIアプリケーションエンジニアにとっては、free-threaded Pythonが成熟すると、multiprocessingを迂回せず、Pythonのマルチスレッドを直接使ってCPU計算を真に並列化できるようになります。AIアプリケーションにおけるデータ前処理や特徴量エンジニアリングなど、CPUバウンドな工程で大きな利点を得られる可能性があります。ただし現時点では、多くのサードパーティライブラリ、特にC拡張を含むライブラリがfree-threadedビルドへ完全には対応していません。本番環境での使用には注意が必要です。
方向2:サブインタープリター
Python 3.14では、PEP 734によってconcurrent.interpretersモジュールが標準ライブラリへ正式に導入されました。サブインタープリターとは、同じプロセス内に存在する複数の独立したPythonインタープリターのインスタンスです。それぞれが独自のGIL、モジュール状態、__main__名前空間を持ちますが、同じプロセスのメモリ空間を共有します。
サブインタープリターは、「スレッドより少し重く、プロセスより少し軽い」並列処理手段と考えられます。multiprocessingと同じく真のマルチコア並列処理を実現できますが、同じプロセス内にあるため作成コストが低く、通信も効率的です。
# Python 3.14+ 的子解释器 API
from concurrent.futures import InterpreterPoolExecutor
def compute(n):
return sum(i * i for i in range(n))
# 类似 ThreadPoolExecutor 的 API,但每个 worker 跑在独立的子解释器里
with InterpreterPoolExecutor(max_workers=4) as pool:
results = list(pool.map(compute, [10_000_000] * 4))
# 四个子解释器各有自己的 GIL,可以真正并行
この設計思想は、ErlangのプロセスモデルとGoのgoroutineから影響を受けています。分離された実行環境が可変状態を共有するのではなく、メッセージパッシングによって通信します。
方向3:asyncioの継続的な進化
asyncio自体も改良が続いています。Python 3.12では構造化された並行処理のためにTaskGroupが導入され、Python 3.11ではasyncio.TaskGroupと例外グループ(ExceptionGroup)が導入され、並行タスクにおけるエラー伝播をより適切に扱えるようになりました。将来的には、asyncioとサブインタープリターの連携を深める方向へ進みます。たとえば、asyncioのイベントループからCPUバウンドなサブタスクをサブインタープリターへ分配し、並列実行する形です。
三つの方向の関係
これらは互いを置き換えるものではありません。より正確には、将来のPythonの並行処理体系は次のようになります。
asyncioは引き続き、I/O並行処理の第一選択肢です。多数のネットワーク接続を管理する際の優位性は変わりません。
CPUバウンドな並列処理については、開発者に三つの選択肢があります。free-threaded Pythonのマルチスレッドは最も単純ですが、スレッド安全性を自分で管理する必要があります。サブインタープリターは分離性が高くAPIも使いやすい一方で、通信には一定の制限があります。multiprocessingは最も成熟していますが、プロセス間通信のコストが最大です。
実際のアプリケーションでは、複数を組み合わせる形 が最も一般的になるでしょう。asyncioがI/Oを管理し、サブインタープリターまたはfree-threadedマルチスレッドがCPUバウンドな部分を処理します。
第9章:まとめ——判断マップ
手段を選ぶための簡潔なルール
I/Oバウンド + 並行数が数十程度: threadingまたはThreadPoolExecutorを使います。単純で直接的に使え、ほとんどのライブラリが対応しています。
I/Oバウンド + 並行数が数百から数千: asyncioを使います。消費するリソースは少なくなりますが、非同期エコシステムのライブラリが必要です。
CPUバウンド + 並列実行が必要: multiprocessingまたはProcessPoolExecutorを使います。現時点で最も成熟した真の並列処理手段です。
asyncio内で同期ブロッキングコードを扱う: asyncio.to_thread()またはloop.run_in_executor()を使って、スレッドプールまたはプロセスプールへ橋渡しします。
Python 3.14+で依存ライブラリが対応済み: ProcessPoolExecutorより軽量な代替手段として、InterpreterPoolExecutorによるサブインタープリターを試し始められます。
中核的な概念の早見表
GIL は、PythonのマルチスレッドがPythonバイトコードを並列実行できないようにしますが、I/O待ちには影響しません。Free-threaded Pythonは、この制約を段階的に解消しつつあります。
並行処理 とは、複数のタスクが交互に進むことです。一人の料理人が三つの料理を進める状況に相当します。並列処理 とは、複数のタスクが同時に実行されることです。三人の料理人がそれぞれ一つの料理を作る状況に相当します。
threading は、Pythonコードについて並行処理を提供しますが、並列処理は提供しません。multiprocessing は真の並列処理を提供します。asyncio は、一つのスレッド内で効率的なI/O並行処理を提供します。concurrent.futures は、スレッドプールとプロセスプールを統一する高レベルインターフェースです。
GILがあってもスレッド安全とは限りません。 マルチスレッドで共有状態を変更する場合、依然としてロックが必要です。asyncioの協調的なスケジューリングは競合のリスクを下げますが、「awaitの前に読み、awaitの後に書く」処理は安全ではありません。
asyncはスケジューリングのプロトコルを担い、スレッドプールとプロセスプールはブロッキングコードを隔離します。 両者は互いを置き換えるのではなく、協調して働きます。