Pythonで共有メモリを使ったプロセス間のデータのやり取りを実装(標準モジュール「mmap」)

お疲れ様です。

Pythonでプロセス間でのデータのやり取りをするにあたり、標準モジュールのmmapを使って共有メモリで実現する方法があります。 2年前くらいに必要があり作ったコードですが、復習の意味も込めて掘り出して再度書いてみたのでこちらにも備忘録として残します。



プロセス間のデータのやり取りについて

python ○○.pyで実行したプロセス内でデータを扱うことになりますが、基本的に個々のプロセス内でしか使えません。構築するシステムによっては複数のプロセスを立ち上げてデータを共有したい場合がでてくるので、その際にはプロセス間通信や共有メモリなどの手法を使う必要があります。

プログラム

プログラムはこちらに置いています。(下記にも詳細を交えて示します。)

github.com

プログラムは送信元のコードと受取先のコードそれぞれを実行して使用します。
内容としては読み込んだ画像を共有メモリに書き込み、受け取り側で読み取った画像を表示するというものになっています。

送信元(process1.py)

LENGTH = 6008
SIZE = (40, 50)

# mmapの準備
mm = mmap(-1, LENGTH, "test_memory")

# 画像読み込み+リサイズ
img = cv2.imread("./img.png")
img = cv2.resize(img, SIZE)
h, w = img.shape[:2]

# 画像と画像の幅・高さをバイト列に変換
byte_img = img.tobytes()
byte_w = struct.pack("i", w)
byte_h = struct.pack("i", h)

# メモリに書き込み
mm.write(byte_img + byte_w + byte_h)
mm.flush()
mm.seek(0)
    
# バイト列の長さを確認
print("Image:", len(byte_img)) # 6000
print("width:", len(byte_w), byte_w)   # 4
print("height:", len(byte_h), byte_h)  # 4

input('push enter to exit.')

mm.close()

mm = mmap(-1, LENGTH, "test_memory")の部分でmmapの準備をしています。定数LENGTHは実際に送るバイト列と同じ長さを指定します。タグとして"test_memory"を指定しており、これで送信元で同じタグを指定することで読み取れるはず…。

画像データはnumpyのtobytesメソッドを使って一度バイト列に変換します。バイト列のままだと幅と高さの情報が失われるので、個別に幅・高さの値もバイト列に変換して送ることになります。単純な数値の変換はPythonの標準モジュールであるstructを使用しています。
※structについてはこちらが参考になります。
【Pyhonでバイナリデータを扱う人必見】structライブラリの基本的な使い方 #Python - Qiita

mm.write()でバイト列を共有メモリに書き込みます。この時最初の部分で指定したLENGTHを超えてしまうと書き込みができないので注意。 ちなみに今回の場合、画像データが40x50にリサイズしたカラー画像(BGR)なので40x50x3=6000、幅・高さの値はstructでint型を変換した時に4byteになるのでそれぞれ4、合計で6008(=LENGTH)となります。

受取先(process2.py)

LENGTH = 6008

# mmapの準備
mm = mmap(-1, LENGTH, "test_memory")

# 共有メモリからバイト列を読み込み
byte_data = mm.read(LENGTH)

# バイト列から目的のデータを抽出
byte_img = byte_data[:6000] 
byte_w = byte_data[6000:6004] 
byte_h = byte_data[6004:6008] 
    
# バイト列のデータを変換
w = struct.unpack("i", byte_w)[0]
h = struct.unpack("i", byte_h)[0]
img = np.frombuffer(byte_img, np.uint8).reshape((h,w,3))
    
# 画像を表示
plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
plt.show()

mm.close()

送信元コードと同様mm = mmap(-1, LENGTH, "test_memory")の部分でmmapの準備。LENGTHとタグの部分は一致させています。

mm.read(LENGTH)で共有メモリからバイト列を読み取ります。この時のLENGTHも準備段階で確保した大きさを超えるとエラーになります。
読み取ったバイト列をスライスで画像データ/幅/高さの該当部分を切り出します。(ここもうちょっとスマートな方法があれば教えてほしい…。)
structを使って幅・高さはint型の数値に変換、画像データはnumpyのfrombufferを使って配列に変換した後、reshapeで成形し画像データの形にしています。

実行結果

下図のようにprocess2.pyの方からmatplotlibで画像が表示されます。一度リサイズを挟んでいるので解像度が低いですが画像データはしっかり送ることができています。

ちなみに元画像は以下です。

実用性

プロセス間のデータのやり取りの方法はいくつかありますが、共有メモリを使用する方法はとにかく高速にデータのやり取りができる点は大きなメリットかなと思います。後は、コードの書き方次第にはなりますが相互にデータのやり取りができるのも便利ですね。
今回はPython同士でデータをやり取りしましたが、共有メモリを扱うことができる他の言語(C++とかC#あたりでしょうか?)とも同じことができるはずです。

一方で送るデータをバイト化した時の長さを計算して指定する必要があるのが少し面倒でしょうか…。ある程度やり取りしたいデータのサイズが決まっている場合(例えば、同じサイズの画像データを扱う場合など)は使いやすいですが、文書など可変サイズのデータは難しそうです。

改良

改良版を作成し記事化しました。こちらもあわせてどうぞ。

fallpoke-tech.hatenadiary.jp