【ITニュース解説】Inter Process Communication in Python Multiprocessing (With Examples)
2025年09月22日に「Dev.to」が公開したITニュース「Inter Process Communication in Python Multiprocessing (With Examples)」について初心者にもわかりやすく解説しています。
ITニュース概要
Pythonのマルチプロセスでは、独立したプロセス間でデータを交換する「プロセス間通信(IPC)」が不可欠だ。Queue, Pipe, Manager, Value, Array, shared_memoryなど複数のツールがあり、それぞれに適した方法でデータを共有する。効率的なデータ連携には、多くの場合伴うデータのシリアライズとその特性を理解することが重要だ。
ITニュース解説
システムにおいて、プログラムが実行される際には「プロセス」という独立した実行単位が用いられる。それぞれのプロセスは通常、互いに独立したメモリ空間を持っており、直接他のプロセスのデータにアクセスすることはできない。この独立性は、システムの安定性やセキュリティを保つ上で非常に重要である。しかし、複数のプロセスが協力して一つの複雑なタスクを処理する必要がある場合、プロセス間でデータを受け渡したり、互いの動作を調整したりする仕組みが不可欠となる。このような仕組みを「Inter-Process Communication」、略してIPCと呼ぶ。Pythonのmultiprocessingモジュールは、プロセス間通信を可能にするために、Queue、Pipe、Manager、Value、Array、そしてSharedMemoryといった様々なツールを提供している。
multiprocessing.Queueは、プロセス間でデータを安全に交換するためのツールである。これは、データが投入された順序で取り出されるという「キュー(待ち行列)」の原理に基づいて動作する。内部的には、パイプとロック機構を組み合わせており、複数のプロセスが同時にデータを追加(put())したり、取り出したり(get())しても、データ競合による問題が発生しないようになっている。Pythonのthreadingモジュールにあるqueue.Queueに似ているが、multiprocessing.Queueは異なるメモリ空間を持つプロセス間での通信向けに設計されている。プロセスがデータをキューに入れる際、データは「ピクル化(シリアライゼーション)」というプロセスを経てバイト列に変換され、パイプを通じて送信される。データを受け取るプロセスでは、そのバイト列が「アンピクル化(デシリアライゼーション)」されて元のPythonオブジェクトに戻される。これは、データを生成するプロセス(プロデューサー)と、そのデータを消費するプロセス(コンシューマー)が非同期に動作するような状況で特に有効である。主な機能として、キューにアイテムを追加するput(item)、アイテムを取り出すget()(キューが空の場合はアイテムが利用可能になるまで待機する)、待機せずにアイテムを取り出そうとするget_nowait()、キュー内のアイテム数を返すqsize()、キューが空であるかを確認するempty()、キューが満杯であるかを確認するfull()などが挙げられる。
multiprocessing.Pipeは、IPCの中でも最も基本的な形式の一つである。これは、ちょうど2つのプロセスを直接結ぶ双方向の通信経路と考えることができる。Pipe()関数を呼び出すと、2つの接続オブジェクトが返される。片方の接続オブジェクトがsend()メソッドを使ってデータを送信すると、もう片方の接続オブジェクトがrecv()メソッドでそのデータを受信できる。Queueが複数のプロセス間での多対多の通信に適しているのに対し、Pipeは厳密に2つのプロセス間でのみ通信を行う、一対一の通信に特化している。主な機能には、オブジェクトを送信するsend(obj)、次に利用可能なオブジェクトを受信するrecv()(データがなければ待機する)、受信待ちのデータがあるかを確認するpoll()、接続端を閉じるclose()などがある。
multiprocessing.Managerは、Pythonのリストや辞書、その他のオブジェクトを複数のプロセス間で安全に共有するための仕組みを提供する。通常、プロセス間でオブジェクトを直接共有することはできないが、Managerは「プロキシオブジェクト」と呼ばれる特殊なオブジェクトを介してこれを可能にする。Managerを起動すると、それは専用のサーバープロセスとして機能し、共有したいオブジェクトを管理する。他のプロセスは、このマネージャーが提供するプロキシオブジェクトを通じて共有オブジェクトにアクセスする。例えば、manager.list()でリストのプロキシを作成し、複数のプロセスからこのプロキシを使ってリストを操作すると、実際のリストはマネージャープロセス内で共有され、その変更は全てのプロセスに反映される。しかし、プロキシを介した間接的な通信と、内部でのピクル化/アンピクル化の処理が必要となるため、QueueやPipeに比べると処理速度は遅くなる傾向がある。主な機能には、マネージャーオブジェクトを開始するManager()、共有リストのプロキシを返すmanager.list()、共有辞書のプロキシを返すmanager.dict()などがある。プロキシオブジェクトは、元のPythonオブジェクトとほとんど同じメソッドをサポートしているため、普段使い慣れたリストや辞書のように操作できる。
multiprocessing.Valueとmultiprocessing.Arrayは、共有メモリ上に特定のC言語データ型の単一の値や固定サイズの配列を作成し、複数のプロセスで直接アクセス・変更できるようにする。これにより、データのピクル化やコピーのオーバーヘッドを避けることができ、高速なデータ共有が可能となる。Valueは整数や浮動小数点数といった単一のスカラー変数を、Arrayは同じ型の要素が並んだ固定サイズの配列を共有メモリに配置する。これらのオブジェクトは.value属性(Valueの場合)やインデックスアクセス(Arrayの場合)を通じて値の読み書きができ、その変更はすぐに全てのプロセスから見えるようになる。データの一貫性を保つため、get_lock()メソッドでロックを取得し、明示的に同期をとることも可能である。Managerと異なり、直接共有メモリに配置されるため、プロキシによる間接的なアクセスがなく、パフォーマンスが向上する。ただし、扱えるデータ型はC言語の基本的な型に限られるという制約がある。
Python 3.8以降で利用可能なmultiprocessing.shared_memoryは、さらに低レベルな共有メモリーブロックを提供する。これはどのプロセスにも属さない独立したメモリ領域であり、データサイエンスや機械学習など、特に大規模なデータセットを扱う場合に非常に強力なツールとなる。このモジュールを使うと、ピクル化やデータのコピー、プロキシを一切介さずに、複数のプロセスが同じメモリーブロックに直接アクセスできる。これにより、非常に効率的なデータ共有、いわゆる「ゼロコピー」を実現できるため、Managerよりもはるかに高速に動作する。主なクラスとして、共有メモリーブロックを表現するSharedMemoryと、共有メモリをバックエンドとするPythonのリストのようなオブジェクトであるShareableListがある。SharedMemoryでは、numpy配列などと組み合わせて、巨大なデータ構造をプロセス間で共有することも可能である。共有メモリはユニークな名前で識別され、作成したプロセスだけでなく、その名前を知っている他のプロセスもアタッチして利用できる。利用が終わったら、close()メソッドでプロセスからデタッチし、最後にunlink()メソッドで共有メモリ領域を解放する必要がある。
これらのIPCツールを比較すると、それぞれ最適な利用シーンが異なることが分かる。Queueはプロデューサー・コンシューマーモデルに最適で、安全かつシンプルである。Pipeはシンプルな二者間通信に適している。ManagerはPythonオブジェクトを共有するのに便利だが、プロキシを介するため速度は劣る。ValueとArrayは単純な数値の状態を共有するのに高速で、低レベルな制御が可能だ。そしてshared_memoryは、特に大規模なデータ(NumPy配列など)をゼロコピーで効率的に共有したい場合に最も適している。
プロセス間でデータをやり取りする際には、「シリアライゼーション(直列化)」という重要な概念を理解する必要がある。各プロセスは独立したメモリ空間を持つため、Pythonはデータをメモリ上のポインタとして直接渡すことはできない。その代わりに、送信側のプロセスでPythonオブジェクトをバイトストリーム(一連のバイナリデータ)に変換する「ピクル化」を行い、そのバイトストリームを通信経路を通じて送信する。受信側のプロセスでは、このバイトストリームを元のPythonオブジェクトに戻す「アンピクル化」が行われる。この「ピクル化 → 転送 → アンピクル化」という一連の処理が、マルチプロセスにおけるIPCの基本的な流れとなる。
Pythonの標準ライブラリであるpickleは、このシリアライゼーションを行うための主要なツールである。int、list、dict、set、tupleといった多くの組み込み型や、グローバルに定義されたクラス、関数などをピクル化できる。しかし、pickleにはいくつかの制約がある。例えば、ローカル関数やラムダ関数、開いているファイルハンドル、ソケット、スレッドロックといったオブジェクトはピクル化できない。また、大規模なオブジェクトや複雑な構造を持つオブジェクトの場合、ピクル化の処理自体が時間のかかるオーバーヘッドとなることがある。
このようなpickleの制約を克服するために、「cloudpickle」という、より強力なシリアライゼーションライブラリも存在する。これはpip install cloudpickleでインストールでき、pickleでは扱えないラムダ関数やネストされた関数、ローカルに定義されたクラスなどもピクル化できる点が特徴だ。cloudpickleは、DaskやRay、PySparkといった分散コンピューティングフレームワークで広く利用されている。
これらのシリアライゼーションの仕組みは、multiprocessing.QueueやPoolを使ってデータを渡したり、プロセスに引数を送ったりする際に非常に重要となる。単純な整数や文字列、リストといったプリミティブなデータ型を渡す場合は問題ないが、ラムダ関数や開いているファイル、カスタムのC拡張オブジェクトのような複雑なオブジェクトを渡そうとすると、pickleの制約によってエラーが発生したり、予期せぬ挙動につながったりすることがある。
特に大規模なデータセット、例えば1GBにもなるNumPy配列などをプロセス間で頻繁にやり取りする場合、ピクル化/アンピクル化のオーバーヘッドは無視できないほど大きくなり、アプリケーションのパフォーマンスを著しく低下させる可能性がある。このような状況のために、Pythonのmultiprocessingは、ピクル化を必要としない「真の共有メモリ」を利用するValue/Arrayや、より低レベルでゼロコピーを実現するshared_memoryといったツールを提供している。これらを適切に選択し利用することが、マルチプロセスアプリケーションの効率と性能を最大化する鍵となる。