👌

Python asyncioのつまずきどころ –プロセスプール–

2022/07/17に公開

はじめに

近年のPythonではasyncioの仕組みが導入され、解説記事も多く書かれています。私はasyncioを腰を据えて勉強しようと思い色んな記事を読んだことがあるんですが、なぜかいつも読んでいるうちに良くわからなくなるんですよね。このたび、ようやく自分の理解が何に躓いていたのかが分かったので、それをまとめます。本記事はasyncioの包括的な解説ではありません。初めての人は、別の記事で勉強を始めることをお勧めします。

asyncio導入の背景

「PythonではGILが導入されており、物理CPUのスペックによらず同時に1つの処理しかできない」という話は聞いたことがあるんじゃないでしょうか。asyncioの解説はまずこの話から始まることが多いんじゃないですかね。(ちなみに、厳密に言うとC言語によるPythonの実装(=CPython)における話なんですね)

CPUで計算を同時に行えないことは受け容れるしかないです。しかし、入出力でCPUが待機状態の間に他の計算を進められないのは非効率であり、入出力だけでも非同期で処理したいという欲求が生じます。そこで導入されたのがasyncioの枠組みです。asyncioというだけあって、あくまでIO(=入出力)を非同期処理化するのであって、同時計算不可の原則には反しません。

プロセスプール

さて、入出力と聞いて通常想像するのはファイルの読み書きやネットワーク通信ではないでしょうか? でも実はもう1つ大事な入出力相手があります。それがプロセスプールです。プロセスプールとは・・・正確な表現じゃないかもしれませんが、今使っているCPUとは別に用意された仮想的なCPU(しかも複数用意されたCPU)だと思うと良いかなと思います。このプロセスプールに処理をしてもらい結果を受け取るようにすれば、不思議なことに、当初述べたPythonの制約をうまくかわして、並列処理が実現できます。

要するに、asyncioの導入は入出力の非同期化が目的だったが、その仕組みの中で計算の並列化も達成できたということです。自分はこの点に気づくことがなかなかできずに理解がおぼつかなかったです。

実装

プロセスプールを用いた並列計算はだいたいこういうパターンになります。some_funcanother_funcの計算を同時に進行させます。注意点として、これら計算対象の関数はasync defでなくdef(つまり同期関数)として定義します。

from concurrent.futures import ProcessPoolExecutor

async def main():
   loop = asyncio.get_running_loop()
   with ProcessPoolExecutor() as pool:
      result1 = await loop.run_in_executor(pool, some_func)
      result2 = await loop.run_in_executor(pool, another_func)
      print(result1, result2)

asyncio.run(main())

まとめ

本記事の要点は太字で強調したところに尽きます。同様の点で躓いていた人の一助になればと思います。

Discussion