トビアス・ヴリグスタッドとフリトヨフ・ストルトは、ドングヒ・ナと共に、Python Language Summit に再び参加しました。トビアスとフリトヨフは、以前に 2025 年の Python Language Summit で「Fearless Concurrency」というテーマを発表していました。今年のテーマは、「Python のマルチスレッド時代の終焉」と、Python が提供する高レベルの並行処理機能についてでした。

「使いやすい」とは必ずしも「良い」とは限らない。

現在、マルチスレッドにアクセスするためのインターフェースは、予想通り「スレッド」です。スレッドは、学校や教科書、その他の馴染みのある教材を通じて、人々が最初に真の並列処理について学ぶ方法です。しかし、もしスレッドがユーザーインターフェースとしてあまり良くないとしたらどうでしょうか?スレッドを使用すると、ユーザーはデッドロックや競合状態について考慮する必要があります。

Python のマルチスレッドプロジェクトは、PEP 779 の採択以来、目覚ましい進歩を遂げています。しかし、PEP 779 の採択基準には、まだ対応されていない側面がありました。それは、高レベルの並行処理機能です。Steering Council の採択声明には、次のように記載されていました。

高レベルの並行処理機能の準備。Python のコアチームは、ユーザーが安全かつ効果的に使用できる、より高レベルの並行処理機能を検討し、提案する必要があります。そのためには、基盤となるスレッドの仕組みを深く理解する必要はありません。Steering Council は、上記のタスクが安定したら、このタスクを優先して取り組むことを望んでいます。この目的には、stdlib の concurrent パッケージを使用することを推奨します。

この分野には、先行技術が存在します。Python は、他のプログラミング言語が提供する機能からヒントを得ることができます。例えば、Rust の所有権と借用、Go のチャネルとゴルーチン、Erlang のアクターとメッセージ送信などがあります。

高速、シンプル、または安全:Python の並行処理は、何を優先すべきか?

この講演は、異なるプログラミング言語で並行処理を実装する際のさまざまなアプローチを、互いに「対立する可能性がある」3 つの軸(速度、シンプルさ、安全性)で比較することから始まりました。

C言語は高速でシンプルですが、ポインタを介してメモリに直接アクセスできるため、危険で安全でない操作も可能になります。C言語は、プログラムの正確性を確保するために、プログラマの自律性に依存しています。Erlangは複数のスレッドを持ちますが、通信にはデータが別の「アクター」にコピーされるため、安全でシンプルですが、パフォーマンスは低下します。Rustは高性能で安全ですが、シンプルではありません。「あなたはしばしば、借用チェッカーとの戦いに直面することになるでしょう」。

「この三角形の中で、Pythonをどこに位置づけるべきでしょうか?」

グローバルインタプリタロック(GIL)を使用するPython(図では単一のロックで表現)は、シンプルなモデルですが、完全に安全ではありません。なぜなら、GILは「ランタイムのみを保護する」からです。サブインタプリタ(図では複数のロックで表現)は、分離されているため、より安全なモデルですが、データのコピーが必要なため、シンプルでも高性能でもありません。最後に、フリースレッドのPython(図では「racecar」と表現)があり、これはC言語に非常に似ています。シンプルで高性能ですが、プログラムの正確性と安全性を確保する責任はプログラマにあります。

彼らは、高レベルなモデルにおいては「安全性が最優先で、次にパフォーマンス」、そして「シンプルさは第三」であると主張しました。「シンプルさとパフォーマンスはトレードオフの関係にあります」。

行動指向並行処理(BOC)

行動指向並行処理(BOC)は、Pythonのための高レベルな並行インターフェースとして、このトリオが提案しているモデルです。BOCモデルで記述されたプログラムは、タスクベースです。タスクは軽量で、どのコアでも実行でき、デッドロックが発生しません。

各タスクは、ミューテックスによって保護されたデータを所有しています。Pythonで慣れ親しんでいるthreading.Lockオブジェクトとは異なり、これらのミューテックスは、保護するデータについて認識しています。このデータ認識により、ミューテックスは、データオブジェクトが一度に単一のタスクによってのみアクセスされるように保証し、分離を提供し、データ競合を防ぎます。BOCでは、これらのミューテックスを「Cowns」と呼び、これは「同時所有者」を意味します。

BOCでは、`@when`デコレータを使用して、タスクが依存するミューテックスを定義できます。このデコレータには、1つ以上のミューテックスを指定でき、タスクは、そのタスクが依存するミューテックスを通じてのみデータにアクセスできます。この仕組みにより、BOCはすべてのタスクとデータにわたる依存関係グラフを作成できます。

上記の制約を満たすプログラムをスケジューラに提供すると、次の3つの重要な利点が得られます。デッドロックの発生がない、データ競合が発生しない、そして適切な「ロックの管理」が実現されます。ユーザーは「スレッドやサブインタープリタの起動を決定する必要はありません」。これは、ランタイムによって処理されます。スケジューラは、検出されたデッドロックやデータ競合を例外に変換することもでき、実行前にこれらの状況を検出できます。

次に、ロックやスレッドを使用して記述されたプログラムの例をいくつか取り上げ、それらをBOCモデルを使用して記述されたプログラムに変換しました。例えば、2つの銀行口座間で送金を行い、その結果を出力するプログラムなどです。

from bocpy import Cown, wait, when
class Account:
def __init__(self, name: str, balance: float):
"""初期残高でアカウントを初期化します。"""
self.name = name
self.balance = balance
def __repr__(self) -> str:
"""デバッグ用の読みやすい表現を返します。"""
return f"Account(name='{self.name}', balance= {self.balance})"
def atomic_transfer(src: Cown[Account], dst: Cown[Account], amount: float):
"""両方のCownがロック解除され、資金がある場合に、``src``から``dst``へ資金を移動します。"""
@when(src, dst)
def do_transfer(src: Cown[Account], dst: Cown[Account], amount=amount):
src_account = src.value
dst_account = dst.value
if src_account.balance > amount:
src_account.balance -= amount
dst_account.balance += amount
def check_balance(message: str, account: Cown[Account]):
"""指定されたアカウントの現在の残高をログに記録します。"""
@when(account)
def do_check(account: Cown[Account], message=message):
print(message, account.value)
alice = Cown(Account("Alice", 100))
bob = Cown(Account("Bob", 0))
check_balance("src (transfer前):", alice)
check_balance("dst (transfer前):", bob)
atomic_transfer(alice, bob, 10)
wait()

GitHubには、さらに多くの例があります。この概念実証は、サブインタープリタを使用して実装されており、誰でも試すことができます。bocpyは、Python Package Indexで利用可能です。

python -m pip install bocpy

bocpyは、シンプルさ、安全性、パフォーマンスという独自の基準に基づいて評価されています。bocpyは、上記の例からもわかるように、シンプルです。安全性の面では、bocpyは現在「安定版のPython」で動作しており、そのため、現時点では分離機能のみが実装されています。「まだ所有権の仕組みはありません。サードパーティライブラリとして[所有権]を実装することはできません」という理由は、ランタイムの変更が必要になるためです。パフォーマンスは、「通信がボトルネックにならないプログラムにとっては良好」ですが、「サブインタープリタでは、通信がコストがかかるためです」。

bocpyパッケージは、次の3つの機能を提供します。

「Cown」、または所有権を持つロック。

動作 ( `@when` デコレータで生成されるタスク)

スケジューラ (サブインタプリタで実装され、将来的にはスレッド化への対応が予定されています)

今後の展望として、このグループは、Pythonランタイムを「安全な並行処理を可能にし、安全な抽象化を構築しながらパフォーマンスを維持する」ように変更する、他の2つのプロトタイプを開発しています。 1つは、オブジェクトを分離されたグループに整理することで分離を実現し、所有権違反が発生するとCownsから例外が発生するようにします。 もう1つは、Pythonオブジェクトの不変性を実現するためのもので、PEP 795に関連しています。

このグループは、安全な並行処理をサポートするためには、Pythonのコア部分の変更が必要であることを明確にし、「パフォーマンスを犠牲にして安全性を得る」のかどうかを問うていました。 標準ライブラリでどのような基本的な機能を提供すべきか、ロックだけで十分なのか。 また、もし基本的な機能を提供する場合、それは `bocpy` のようなものになるべきか。

議論

トーマス・ワウテルスは、コアチームが「サブプロセス、マルチプロセッシング、スレッディングのための汎用的なインターフェースを作成しようとした経験がある」と述べました。 しかし、実際には常に例外的なケースがあり、APIの制約によりパフォーマンスが最適ではないという問題があります。 トーマスは「彼ら(3人)は、`bocpy`についても同じことが言えないとどれだけ確信しているのか」と質問しました。 3人はトーマスの懸念を共有していました。 「これは現在取り組んでいる問題です」とフリトヨフは答えました。「`bocpy` の所有権モデルを使用すると、サブインタプリタとスレッドを同様に扱うことができ、通信が可能になり、所有権モデルを通じてオブジェクトを直接共有できます。」

「タスクとスケジューラという言葉を聞くと、すぐに非同期処理を思い浮かべる」とデイビッド・ヒューイットは述べ、さらに「非同期処理は、この状況の中でどのように位置づけられるのだろうか?」と疑問を呈しました。彼は、bocpyが「asyncio上に構築されるべきか」であり、「同期的な実装ではなく、非同期のミューテックスプリミティブを使用すべきか」について、そのグループのメンバーに質問しました。トービアスは、bocpyが「asyncioを使って構築できる可能性がある」と答えました。「それは、使用するバックエンドのインフラストラクチャによって異なり、それぞれのバックエンドにはトレードオフがある」からです。「もしI/O処理を行いたい場合、データが利用可能になったら、I/Oライブラリに何らかの処理を指示し、データが利用可能になったときに実行するタスクをスケジュールすることで、ブロックを回避できます」。

ラリー・ハスティングスは、「この研究が進められているのを見て嬉しい」と述べ、そのグループからのさらなる情報に期待を示しましたが、bocpyや特定の解決策を「Pythonで並行処理を行うための唯一の方法」とは考えていませんでした。「ラリーは、Pythonが、競合するアイデアを持つあなたや他のグループが、アイデアを実装し、ユーザーに提供できるようにするためのすべてのツールを持っていることを望んでいます」。代わりに、ラリーは「単一の方法を押し付ける」ことは、より良い選択肢が後で発見された場合に、Pythonを特定の具体的な実装に固定してしまう可能性があるため、慎重であるべきだと考えていました。ドンヒは、そのグループが当初は単一の方法を強制しようとしていたのではなく、あくまで議論を始めることを目的としていたと述べました。

フリトヨフは、Rustを例に挙げ、非同期とawaitの上に高レベルのプリミティブを提供しなかった結果、Rustのエコシステムに「分裂」が生じたことを指摘しました。「活気があるようですね」とラリーは答えました。

デイビッド・ヒューイットは、Rustコミュニティの多くが、「Rustの非同期処理の状況」を「ある意味でうまくいかなかった」と考えていると述べました。それは、標準ライブラリにランタイムのための標準的なインターフェースが欠けていたためです。その結果、Rustのエコシステム全体が「Tokioに収束するよう」に「強制」されてしまいました。デイビッドは、トーマスが言うように、「優れた抽象化を考案するのは難しいだろう」と同意しましたが、「それは、試すべきではないという意味ではありません」。