Python講座 / スレッド・プロセス・asyncioを選ぶ
スレッド、プロセス、asyncioは複数の処理を進める異なる方法です。CPU計算か外部の待ち時間か、対応ライブラリは何かを見て選び、並行化で速くなると決めつけず実際の処理を測定します。
並行と同時実行の条件を整理する
| 方式 | モデル | 主な費用や条件 |
|---|---|---|
| スレッド | 同一プロセスのメモリーを共有 | 同期とライブラリの動作 |
| プロセス | 別々のメモリー領域 | 起動、直列化、データ転送 |
| asyncio | awaitで協調的に進行 | 非同期対応とループの停止防止 |
通常のGIL付きCPythonでは、スレッドを増やすだけで任意の純PythonのCPU計算を並列化できません。GILを解放する拡張やfree-threadedビルドでは条件が変わるため、インタープリターのビルドとライブラリを合わせて考えます。
待機処理の制限と取得失敗は、HTTP APIとJSON応答・通信エラーを扱うで扱う規則とつながります。
同じ全体例で呼び出し方を比較する
concurrency.pyとして保存し、python concurrency.pyを実行します。ノートブックや対話環境でもプロセスが同じように動くとは考えず、.pyファイルを使います。
import asyncio
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
def square(value):
return value * value
async def async_square(value):
await asyncio.sleep(0.01)
return square(value)
async def gather_values():
return await asyncio.gather(*(async_square(x) for x in [1, 2, 3]))
def main():
with ThreadPoolExecutor(max_workers=2) as pool:
print(list(pool.map(square, [1, 2, 3])))
with ProcessPoolExecutor(max_workers=2) as pool:
print(list(pool.map(square, [1, 2, 3])))
print(asyncio.run(gather_values()))
if __name__ == "__main__":
main()
予想結果です。
[1, 4, 9] [1, 4, 9] [1, 4, 9]
起動の入口と協調的な待ちを守る
Windowsなどのプロセス起動ではメインモジュールが読み込まれます。mainガードは再帰的な起動を避けます。squareをモジュール直下で定義すると、子プロセスから参照できます。
async関数の中でも、長いCPU計算や同期通信を譲らず実行するとイベントループを止めます。asyncio.sleepは協調的な待ちの例で、実際のHTTP取得はしていません。既に動作しているイベントループ内へasyncio.runを入れ子にできません。
実際の負荷と失敗処理を設計する
小さな二乗の例はAPIの比較で、速度のベンチマークではありません。プロセスの起動が計算より高くつくこともあります。代表的なデータで環境、ワーカー数、メモリーも記録して測定します。
mapの結果は入力順ですが、完了順は別の場合があります。同時数を制限し、共有状態の同期、例外、取り消し、終了を設計します。無制限にタスクを作ったり、同期なしの共有リスト変更へ依存したりしないでください。API収集実習の通信と保存の段階を理解してから、並行リクエストの必要性を判断します。
GIL(Global Interpreter Lock)は、通常のGIL有効CPythonで複数スレッドによるPythonバイトコードの同時実行を制限するロックです。ブロッキングI/Oはスレッド、純粋なPythonのCPU処理はプロセス、非同期対応I/Oはasyncioを検討し、実際の負荷で測ります。free-threadedビルドやGILを解放する拡張には別の条件があります。二乗の例はAPIの練習で速度比較ではありません。タスク数・待ち時間を制限し、Future.resultの例外も確認します。下の最小例はfinallyの片付け後に呼び出し元がキャンセルを確認します。
全体を cancel.py に保存し、同じ練習フォルダーで python cancel.py を実行します。
import asyncio
async def worker():
try:
await asyncio.sleep(60)
finally:
print("worker cleanup")
async def main():
task = asyncio.create_task(worker())
await asyncio.sleep(0)
task.cancel()
try:
await task
except asyncio.CancelledError:
print("cancel confirmed")
if __name__ == "__main__":
asyncio.run(main())
予想結果:
worker cleanup cancel confirmed
次は投入したタスクの失敗をresultで確認する例です。max_workersは同時実行数で、待ち行列全体の上限ではありません。timeoutは待ち時間を制限するだけで実行中の処理を強制停止しません。大量の入力は小さなバッチに分け、asyncioの同時I/OにはSemaphoreなどで別の上限を設けます。
全体を future_failure.py に保存し、同じ練習フォルダーで python future_failure.py を実行します。
from concurrent.futures import ThreadPoolExecutor
def calculate(value):
if value < 0:
raise ValueError("negative input")
return value * value
with ThreadPoolExecutor(max_workers=2) as pool:
futures = [pool.submit(calculate, value) for value in [2, -1]]
for future in futures:
try:
print(future.result(timeout=5))
except ValueError as error:
print("failed:", error)
予想結果:
4 failed: negative input
動作と適用条件の公式説明:Python: asyncio task cancellation · Python: concurrent.futures




