標準ライブラリ内の複数のインタプリタ[3.14]
Python 3.14では、同一プロセス内で複数のインタプリタを実行することがモジュールレベルで可能になりました。
従来は、この機能はC-API(C言語から利用することを前提としたAPI群)においてのみ利用可能でしたが、concurrent.interpretersモジュールが提供されるようになり、Pythonコードから直接複数インタプリタを利用できるようになりました。
同一プロセス内で複数のインタプリタを実行する意味は、上記のグローバルインタプリタロックが有効なインタプリタにあります。このインタプリタは、マルチスレッド処理を十分に高速化できないといった課題がありました。
そこで、インタプリタ自体をスレッドとして起動し、シングルスレッド実行でマルチコアCPUのメリットを最大限に生かそうというのが複数インタプリタです。
ベーシックなconcurrent.interpretersモジュール
concurrent.interpretersモジュールは、インタプリタの起動や関数の呼び出し、コードの実行などを司ります。ベーシックな機能を提供しており、とりあえず複数のインタプリタを使いたいのであれば、このモジュールを使うことになります。
基本的には、インタプリタのためのスレッドを起動し、その中でInterpreterインスタンスを生成、そのインスタンスに対して関数の呼び出しやコードの実行、__main__モジュールに対するオブジェクトの引き渡しを行っていきます。
以下のリストは、この手順でインタプリタを複数回起動して関数を呼び出し、情報を表示するものです。
import concurrent.interpreters as interpreters
import os, threading
def worker(arg):
print(f"Start thread: id={threading.get_ident()}", flush=True)
# 新しい Python インタプリタ(サブインタプリタ)を作成
interp = interpreters.create() (1)
print(f"Start interpreter: id={interp.id}", flush=True) (2)
# サブインタプリタ内で実行する関数を定義
def func(arg): (3)
print(f"Start function: arg={arg}", flush=True)
return arg
# サブインタプリタ内でfuncを呼び出して結果を表示
result = interp.call(func, arg) (4)
print(f"Interpreter result: {result}", flush=True)
# サブインタプリタを閉じてリソースを解放
interp.close() (5)
if __name__ == "__main__":
args = [100, 200, 300]
print(f"CPython pid={os.getpid()}, thread id={threading.get_ident()}", flush=True)
# それぞれの引数ごとにスレッドを作成&起動
threads = [threading.Thread(target=worker, args=(i,)) for i in args]
for t in threads:
t.start()
Interpreterクラスのメソッドについて補足します。(1)はインスタンスの生成で基本的にcreateメソッドでインスタンスを生成して使うことになります。(2)でインタプリタのIDを取得でき、(4)のcallメソッドで(3)のような関数を引数付きで呼び出し、戻り値を受け取ることができます。不要になったインタプリタは(5)のようにcloseメソッドで破棄します。
実行すると、以下のようになります(実行のたびに表示順は変化します)。インタプリタのIDは1からの整数値となるようです。
CPython pid=30549, thread id=140704305471680 Start thread: id=123145505316864 Start thread: id=123145522106368 Start thread: id=123145538895872 Start interpreter: id=1 Start function: arg=100 Interpreter result: 100 Start interpreter: id=3 Start interpreter: id=2 Start function: arg=300 Interpreter result: 300 Start function: arg=200 Interpreter result: 200
ちなみに、callメソッドの代わりに、スクリプトを文字列で渡して実行できるexecメソッド、スレッドを生成して関数を呼び出せるcall_in_threadメソッドも利用可能です。
より抽象化されたconcurrent.futuresモジュール
concurrent.interpretersはベーシックな機能を提供してくれますが、スレッドの起動が必要など手順がやや煩雑で、またスレッドやプロセスといった並列化とは手順が異なるのが難点です。
そこで、並列化の機能を提供するconcurrent.futuresモジュールに新たにInterpreterPoolExecutorクラスが提供されるようになりました。InterpreterPoolExecutorクラスは、従来からのThreadPoolExecutor、ProcessPoolExecutorと同様のインタフェース(例えばmapなど)で使うことができ、より容易に複数インタプリタを利用できます。
以下のリストは、ある自然数を上限とした素数をカウントする関数を、別インタプリタで複数回呼び出して情報を表示するものです。
from concurrent.futures import InterpreterPoolExecutor
import os, threading, sys, math
# 素数カウント(処理内容は複数インタプリタと無関係なので省略)
def count_primer(upper: int):
print(f"Start interpreter: upper={upper}, thread id={threading.get_ident()} main id={id(sys.modules['__main__'])}", flush=True) (1)
…略…
print('End', flush=True)
return count
if __name__ == "__main__":
# 並列に処理したいタスクの入力(それぞれ「上限値(upper)」)
pattern = [1_000_000, 500_000, 100_000]
print(f"CPython pid={os.getpid()}, main id={id(sys.modules['__main__'])}", flush=True)
# InterpreterPoolExecutorで並列にタスクを実行
with InterpreterPoolExecutor() as pool: (2)
results = list(pool.map(count_primer, pattern))
# 並列処理の結果をまとめて表示
print(f"Results: {results}", flush=True)
InterpreterPoolExecutorクラスのメソッドなどについても補足します。
(1)においては、起動したインタプリタおよび呼び出した関数の情報を出力しています。関数の引数(上限の自然数)、スレッドID、__main__モジュールのIDを出力させて、インタプリタとスレッドの情報を得ています。
(2)以降がインタプリタの起動部分で、InterpreterPoolExecutorクラスのインスタンスからmapメソッドで関数を呼び出し、第2引数に3要素のリストを与えることで3回インタプリタを起動します。全て実行が終了すれば、結果をリストにして出力して終了です。
実行すると、以下のようになります(実行のたびに表示順は変化します)。
CPython pid=31133, main id=4444602128 Start interpreter: upper=1000000, thread id=123145451397120 main id=4458833504 Start interpreter: upper=500000, thread id=123145468186624 main id=4459882080 Start interpreter: upper=100000, thread id=123145484976128 main id=4460930656 End End End Results: [78498, 41538, 9592]
注目すべきはmain idで、ここでは__main__モジュールのIDをsys.modulesから取得していますが、3回の関数呼び出しで全てIDが異なることが分かります。また、各インタプリタは別スレッドで起動するので、スレッドIDも変わってきます。
